From 437f42250e381ab7652e07b4a413bb5d152356e1 Mon Sep 17 00:00:00 2001 From: Wes van der Vleuten <16665772+WesVleuten@users.noreply.github.com> Date: Mon, 7 Nov 2022 03:49:00 +0100 Subject: [PATCH 001/598] Watched marker --- src/invidious/views/components/item.ecr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr index 0e959ff2..e53fa075 100644 --- a/src/invidious/views/components/item.ecr +++ b/src/invidious/views/components/item.ecr @@ -99,7 +99,7 @@ <% else %> <% if !env.get("preferences").as(Preferences).thin_mode %> -
+
"> <% if env.get? "show_watched" %>
" method="post"> From 7b573817734dfd48fc6d1fbdc9a0a99f379f0ed1 Mon Sep 17 00:00:00 2001 From: Wes van der Vleuten <16665772+WesVleuten@users.noreply.github.com> Date: Mon, 7 Nov 2022 19:03:23 +0000 Subject: [PATCH 002/598] Added watch indicator --- assets/css/default.css | 13 ++++++++++ assets/js/watched_widget.js | 27 +++++++++++++++++++++ docker-compose.yml | 4 +-- src/invidious/views/components/item.ecr | 7 +++++- src/invidious/views/feeds/subscriptions.ecr | 3 ++- 5 files changed, 50 insertions(+), 4 deletions(-) diff --git a/assets/css/default.css b/assets/css/default.css index ab2b79e6..30a562e2 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -135,6 +135,9 @@ div.thumbnail { position: relative; box-sizing: border-box; } +div.thumbnail.thumbnail-watched { + background-color: rgba(255,255,255,.4); +} img.thumbnail { position: absolute; @@ -143,6 +146,16 @@ img.thumbnail { left: 0; top: 0; object-fit: cover; + z-index: -1; +} + +div.watched-indicator { + position: absolute; + left: 0; + bottom: 0; + height: 4px; + width: 100%; + background: red; } .length { diff --git a/assets/js/watched_widget.js b/assets/js/watched_widget.js index f1ac9cb4..10b33c1a 100644 --- a/assets/js/watched_widget.js +++ b/assets/js/watched_widget.js @@ -32,3 +32,30 @@ function mark_unwatched(target) { } }); } + + +var save_player_pos_key = 'save_player_pos'; + +function get_all_video_times() { + return helpers.storage.get(save_player_pos_key) || {}; +} + +var watchedIndicators = document.getElementsByClassName('watched-indicator'); +for (var i = 0; i < watchedIndicators.length; i++) { + var indicator = watchedIndicators[i]; + + var watched_part = get_all_video_times()[indicator.getAttribute('data-id')]; + var total = parseInt(indicator.getAttribute('data-length'), 10); + + var percentage = Math.round((watched_part / total) * 100); + + + if (percentage < 5) { + percentage = 5; + } + if (percentage > 90) { + percentage = 100; + } + + indicator.style.width = percentage + '%'; +} diff --git a/docker-compose.yml b/docker-compose.yml index eb83b020..48ee6a4b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,7 +13,7 @@ services: dockerfile: docker/Dockerfile restart: unless-stopped ports: - - "127.0.0.1:3000:3000" + - "3003:3000" environment: # Please read the following file for a comprehensive list of all available # configuration options and their associated syntax: @@ -23,7 +23,7 @@ services: dbname: invidious user: kemal password: kemal - host: invidious-db + host: invidious-invidious-db-1 port: 5432 check_tables: true # external_port: diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr index e53fa075..d63dca14 100644 --- a/src/invidious/views/components/item.ecr +++ b/src/invidious/views/components/item.ecr @@ -99,7 +99,8 @@ <% else %> <% if !env.get("preferences").as(Preferences).thin_mode %> -
"> + <% item_watched = env.get("user") && env.get("user").as(User).watched && env.get("user").as(User).watched.index(item.id) != nil %> +
"> <% if env.get? "show_watched" %> " method="post"> @@ -124,6 +125,10 @@ <% elsif item.length_seconds != 0 %>

<%= recode_length_seconds(item.length_seconds) %>

<% end %> + + <% if item_watched %> +
+ <% end %>
<% end %>

<%= HTML.escape(item.title) %>

diff --git a/src/invidious/views/feeds/subscriptions.ecr b/src/invidious/views/feeds/subscriptions.ecr index 8d56ad14..add1eefc 100644 --- a/src/invidious/views/feeds/subscriptions.ecr +++ b/src/invidious/views/feeds/subscriptions.ecr @@ -50,7 +50,6 @@ }.to_pretty_json %> -
<% videos.each do |item| %> @@ -58,6 +57,8 @@ <% end %>
+ +
<% if page > 1 %> From f604c1c68bbba81310ca2fd0a7283482840e0a26 Mon Sep 17 00:00:00 2001 From: Wes van der Vleuten <16665772+WesVleuten@users.noreply.github.com> Date: Tue, 8 Nov 2022 23:15:42 +0100 Subject: [PATCH 003/598] Fixed thumbnails with darkreader, Added watched indicator in more locations --- assets/css/default.css | 16 +++++++++++----- assets/js/watched_widget.js | 4 +--- src/invidious/views/components/item.ecr | 16 ++++++++++++++-- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/assets/css/default.css b/assets/css/default.css index 30a562e2..890bd524 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -135,9 +135,7 @@ div.thumbnail { position: relative; box-sizing: border-box; } -div.thumbnail.thumbnail-watched { - background-color: rgba(255,255,255,.4); -} + img.thumbnail { position: absolute; @@ -146,7 +144,15 @@ img.thumbnail { left: 0; top: 0; object-fit: cover; - z-index: -1; +} + +div.watched-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(255,255,255,.4); } div.watched-indicator { @@ -155,7 +161,7 @@ div.watched-indicator { bottom: 0; height: 4px; width: 100%; - background: red; + background-color: red; } .length { diff --git a/assets/js/watched_widget.js b/assets/js/watched_widget.js index 10b33c1a..ffcdaad8 100644 --- a/assets/js/watched_widget.js +++ b/assets/js/watched_widget.js @@ -41,15 +41,13 @@ function get_all_video_times() { } var watchedIndicators = document.getElementsByClassName('watched-indicator'); +console.log('indicators', watchedIndicators.length); for (var i = 0; i < watchedIndicators.length; i++) { var indicator = watchedIndicators[i]; - var watched_part = get_all_video_times()[indicator.getAttribute('data-id')]; var total = parseInt(indicator.getAttribute('data-length'), 10); - var percentage = Math.round((watched_part / total) * 100); - if (percentage < 5) { percentage = 5; } diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr index d63dca14..47d077cf 100644 --- a/src/invidious/views/components/item.ecr +++ b/src/invidious/views/components/item.ecr @@ -1,3 +1,5 @@ +<% item_watched = !item.is_a?(SearchChannel) && !item.is_a?(SearchPlaylist) && !item.is_a?(InvidiousPlaylist) && !item.is_a?(Category) && env.get("user") && env.get("user").as(User).watched && env.get("user").as(User).watched.index(item.id) != nil %> +
<% case item when %> @@ -40,6 +42,11 @@ <% if item.length_seconds != 0 %>

<%= recode_length_seconds(item.length_seconds) %>

<% end %> + + <% if item_watched %> +
+
+ <% end %>
<% end %>

<%= HTML.escape(item.title) %>

@@ -67,6 +74,11 @@ <% elsif item.length_seconds != 0 %>

<%= recode_length_seconds(item.length_seconds) %>

<% end %> + + <% if item_watched %> +
+
+ <% end %>
<% end %>

<%= HTML.escape(item.title) %>

@@ -99,8 +111,7 @@ <% else %>
<% if !env.get("preferences").as(Preferences).thin_mode %> - <% item_watched = env.get("user") && env.get("user").as(User).watched && env.get("user").as(User).watched.index(item.id) != nil %> -
"> +
<% if env.get? "show_watched" %> " method="post"> @@ -127,6 +138,7 @@ <% end %> <% if item_watched %> +
<% end %>
From c95ee10d6915bd1bb42e8e81f85848f1ad7b6240 Mon Sep 17 00:00:00 2001 From: Wes van der Vleuten <16665772+WesVleuten@users.noreply.github.com> Date: Tue, 8 Nov 2022 23:18:24 +0100 Subject: [PATCH 004/598] Added parital watch indicator on more locations --- src/invidious/views/add_playlist_items.ecr | 2 ++ src/invidious/views/channel.ecr | 2 ++ src/invidious/views/edit_playlist.ecr | 2 ++ src/invidious/views/feeds/playlists.ecr | 2 ++ src/invidious/views/feeds/popular.ecr | 2 ++ src/invidious/views/feeds/trending.ecr | 2 ++ src/invidious/views/hashtag.ecr | 2 ++ src/invidious/views/playlist.ecr | 2 ++ src/invidious/views/playlists.ecr | 2 ++ src/invidious/views/search.ecr | 2 ++ 10 files changed, 20 insertions(+) diff --git a/src/invidious/views/add_playlist_items.ecr b/src/invidious/views/add_playlist_items.ecr index 22870317..70575de3 100644 --- a/src/invidious/views/add_playlist_items.ecr +++ b/src/invidious/views/add_playlist_items.ecr @@ -39,6 +39,8 @@ <% end %>
+ + <% if query %> <%- query_encoded = URI.encode_www_form(query.text, space_to_plus: true) -%>
diff --git a/src/invidious/views/channel.ecr b/src/invidious/views/channel.ecr index dea86abe..1295423e 100644 --- a/src/invidious/views/channel.ecr +++ b/src/invidious/views/channel.ecr @@ -110,6 +110,8 @@ <% end %>
+ +
<% if page > 1 %> diff --git a/src/invidious/views/edit_playlist.ecr b/src/invidious/views/edit_playlist.ecr index 89819ef0..100764c7 100644 --- a/src/invidious/views/edit_playlist.ecr +++ b/src/invidious/views/edit_playlist.ecr @@ -62,6 +62,8 @@ <% end %>
+ +
<% if page > 1 %> diff --git a/src/invidious/views/feeds/playlists.ecr b/src/invidious/views/feeds/playlists.ecr index a59344c4..f9064762 100644 --- a/src/invidious/views/feeds/playlists.ecr +++ b/src/invidious/views/feeds/playlists.ecr @@ -32,3 +32,5 @@ <%= rendered "components/item" %> <% end %>
+ + diff --git a/src/invidious/views/feeds/popular.ecr b/src/invidious/views/feeds/popular.ecr index e77f35b9..919002cd 100644 --- a/src/invidious/views/feeds/popular.ecr +++ b/src/invidious/views/feeds/popular.ecr @@ -16,3 +16,5 @@ <%= rendered "components/item" %> <% end %>
+ + diff --git a/src/invidious/views/feeds/trending.ecr b/src/invidious/views/feeds/trending.ecr index a35c4ee3..76218165 100644 --- a/src/invidious/views/feeds/trending.ecr +++ b/src/invidious/views/feeds/trending.ecr @@ -45,3 +45,5 @@ <%= rendered "components/item" %> <% end %>
+ + diff --git a/src/invidious/views/hashtag.ecr b/src/invidious/views/hashtag.ecr index 0ecfe832..6064af74 100644 --- a/src/invidious/views/hashtag.ecr +++ b/src/invidious/views/hashtag.ecr @@ -24,6 +24,8 @@ <%- end -%>
+ +
<%- if page > 1 -%> diff --git a/src/invidious/views/playlist.ecr b/src/invidious/views/playlist.ecr index df3112db..1df047ba 100644 --- a/src/invidious/views/playlist.ecr +++ b/src/invidious/views/playlist.ecr @@ -106,6 +106,8 @@ <% end %>
+ +
<% if page > 1 %> diff --git a/src/invidious/views/playlists.ecr b/src/invidious/views/playlists.ecr index c8718e7b..6ce8b033 100644 --- a/src/invidious/views/playlists.ecr +++ b/src/invidious/views/playlists.ecr @@ -96,6 +96,8 @@ <% end %>
+ +
diff --git a/src/invidious/views/search.ecr b/src/invidious/views/search.ecr index 254449a1..c4960d08 100644 --- a/src/invidious/views/search.ecr +++ b/src/invidious/views/search.ecr @@ -37,6 +37,8 @@
<%- end -%> + +
<%- if query.page > 1 -%> From 5bcb5f3175247234c63e71ab3b35b7c3574a8fba Mon Sep 17 00:00:00 2001 From: Wes van der Vleuten <16665772+WesVleuten@users.noreply.github.com> Date: Tue, 8 Nov 2022 23:19:27 +0100 Subject: [PATCH 005/598] Removed console.log --- assets/js/watched_widget.js | 1 - 1 file changed, 1 deletion(-) diff --git a/assets/js/watched_widget.js b/assets/js/watched_widget.js index ffcdaad8..fb4275a3 100644 --- a/assets/js/watched_widget.js +++ b/assets/js/watched_widget.js @@ -41,7 +41,6 @@ function get_all_video_times() { } var watchedIndicators = document.getElementsByClassName('watched-indicator'); -console.log('indicators', watchedIndicators.length); for (var i = 0; i < watchedIndicators.length; i++) { var indicator = watchedIndicators[i]; var watched_part = get_all_video_times()[indicator.getAttribute('data-id')]; From c03f92baf7d2a46c3a9ec91c75da3f6d3d24ca57 Mon Sep 17 00:00:00 2001 From: Wes van der Vleuten <16665772+WesVleuten@users.noreply.github.com> Date: Tue, 8 Nov 2022 23:22:44 +0100 Subject: [PATCH 006/598] Fixed watch indicator when position is not saved --- assets/js/watched_widget.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/assets/js/watched_widget.js b/assets/js/watched_widget.js index fb4275a3..3cf7c332 100644 --- a/assets/js/watched_widget.js +++ b/assets/js/watched_widget.js @@ -45,6 +45,9 @@ for (var i = 0; i < watchedIndicators.length; i++) { var indicator = watchedIndicators[i]; var watched_part = get_all_video_times()[indicator.getAttribute('data-id')]; var total = parseInt(indicator.getAttribute('data-length'), 10); + if (watched_part === undefined) { + watched_part = total; + } var percentage = Math.round((watched_part / total) * 100); if (percentage < 5) { From d3d9cfdd0d1d0739b88e34fbb39653131d475665 Mon Sep 17 00:00:00 2001 From: Wes van der Vleuten <16665772+WesVleuten@users.noreply.github.com> Date: Wed, 9 Nov 2022 00:32:38 +0100 Subject: [PATCH 007/598] Cleanup --- assets/css/default.css | 1 - assets/js/watched_widget.js | 1 - docker-compose.yml | 4 ++-- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/assets/css/default.css b/assets/css/default.css index 890bd524..9edf3efa 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -136,7 +136,6 @@ div.thumbnail { box-sizing: border-box; } - img.thumbnail { position: absolute; width: 100%; diff --git a/assets/js/watched_widget.js b/assets/js/watched_widget.js index 3cf7c332..d1b55d28 100644 --- a/assets/js/watched_widget.js +++ b/assets/js/watched_widget.js @@ -33,7 +33,6 @@ function mark_unwatched(target) { }); } - var save_player_pos_key = 'save_player_pos'; function get_all_video_times() { diff --git a/docker-compose.yml b/docker-compose.yml index 48ee6a4b..eb83b020 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,7 +13,7 @@ services: dockerfile: docker/Dockerfile restart: unless-stopped ports: - - "3003:3000" + - "127.0.0.1:3000:3000" environment: # Please read the following file for a comprehensive list of all available # configuration options and their associated syntax: @@ -23,7 +23,7 @@ services: dbname: invidious user: kemal password: kemal - host: invidious-invidious-db-1 + host: invidious-db port: 5432 check_tables: true # external_port: From 1aaf290814e4737142c382c148ede816ca1a5df2 Mon Sep 17 00:00:00 2001 From: shironeko Date: Thu, 29 Dec 2022 14:41:17 -0500 Subject: [PATCH 008/598] handle auto theme correctly with the manual toggle If the user used the manual toggle, they will not be able to get back to auto since it will force set to light theme. This should fix that. --- assets/js/themes.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/assets/js/themes.js b/assets/js/themes.js index 76767d5f..84a9f6d9 100644 --- a/assets/js/themes.js +++ b/assets/js/themes.js @@ -22,9 +22,11 @@ function setTheme(theme) { if (theme === THEME_DARK) { toggle_theme.children[0].className = 'icon ion-ios-sunny'; document.body.className = 'dark-theme'; - } else { + } else if (theme === THEME_LIGHT) { toggle_theme.children[0].className = 'icon ion-ios-moon'; document.body.className = 'light-theme'; + } else { + document.body.className = 'no-theme'; } } From 855202e40e09af1cb5fb372d4a2d05a61b3a9bb2 Mon Sep 17 00:00:00 2001 From: Gavin Johnson Date: Mon, 16 Jan 2023 15:40:38 -0800 Subject: [PATCH 009/598] 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 4aa696fa6ea11959e72b7c084d0b55b2c5476a07 Mon Sep 17 00:00:00 2001 From: Wes van der Vleuten <16665772+WesVleuten@users.noreply.github.com> Date: Sat, 21 Jan 2023 23:08:24 +0100 Subject: [PATCH 010/598] Update assets/js/watched_widget.js with suggestion of AHOHNMYC Co-authored-by: AHOHNMYC <24810600+AHOHNMYC@users.noreply.github.com> --- assets/js/watched_widget.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/assets/js/watched_widget.js b/assets/js/watched_widget.js index d1b55d28..02537111 100644 --- a/assets/js/watched_widget.js +++ b/assets/js/watched_widget.js @@ -39,11 +39,9 @@ function get_all_video_times() { return helpers.storage.get(save_player_pos_key) || {}; } -var watchedIndicators = document.getElementsByClassName('watched-indicator'); -for (var i = 0; i < watchedIndicators.length; i++) { - var indicator = watchedIndicators[i]; - var watched_part = get_all_video_times()[indicator.getAttribute('data-id')]; - var total = parseInt(indicator.getAttribute('data-length'), 10); +document.querySelectorAll('.watched-indicator').forEach(function (indicator) { + var watched_part = get_all_video_times()[indicator.dataset.id]; + var total = parseInt(indicator.dataset.length, 10); if (watched_part === undefined) { watched_part = total; } @@ -57,4 +55,4 @@ for (var i = 0; i < watchedIndicators.length; i++) { } indicator.style.width = percentage + '%'; -} +}); From 7fd205179b5707a2774d83866f5a35b2bd8cfe16 Mon Sep 17 00:00:00 2001 From: Wes van der Vleuten <16665772+WesVleuten@users.noreply.github.com> Date: Sat, 21 Jan 2023 23:24:22 +0100 Subject: [PATCH 011/598] Added suggestions --- src/invidious/views/components/item.ecr | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr index 47d077cf..fa12374f 100644 --- a/src/invidious/views/components/item.ecr +++ b/src/invidious/views/components/item.ecr @@ -1,4 +1,4 @@ -<% item_watched = !item.is_a?(SearchChannel) && !item.is_a?(SearchPlaylist) && !item.is_a?(InvidiousPlaylist) && !item.is_a?(Category) && env.get("user") && env.get("user").as(User).watched && env.get("user").as(User).watched.index(item.id) != nil %> +<% item_watched = !item.is_a?(SearchChannel | SearchPlaylist | InvidiousPlaylist | Category) && env.get?("user").try &.as(User).watched.index(item.id) != nil %>
@@ -42,7 +42,7 @@ <% if item.length_seconds != 0 %>

<%= recode_length_seconds(item.length_seconds) %>

<% end %> - + <% if item_watched %>
@@ -74,7 +74,7 @@ <% elsif item.length_seconds != 0 %>

<%= recode_length_seconds(item.length_seconds) %>

<% end %> - + <% if item_watched %>
From 96344f28b4b842e915325aef64bc93fc9fc55387 Mon Sep 17 00:00:00 2001 From: Gavin Johnson Date: Sat, 28 Jan 2023 09:26:16 -0800 Subject: [PATCH 012/598] 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 013/598] 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 014/598] 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 e7a9aeff9538903d22363d2abcee28c62dc10895 Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Mon, 30 Jan 2023 10:49:23 -0500 Subject: [PATCH 015/598] Add username to auth token callback --- src/invidious/routes/account.cr | 1 + 1 file changed, 1 insertion(+) diff --git a/src/invidious/routes/account.cr b/src/invidious/routes/account.cr index 9bb73136..d01aee56 100644 --- a/src/invidious/routes/account.cr +++ b/src/invidious/routes/account.cr @@ -262,6 +262,7 @@ module Invidious::Routes::Account end query["token"] = access_token + query["username"] = user.email url.query = query.to_s env.redirect url.to_s From bf5175d1e979005f6d04c9d7639c9db4aa08fb7b Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Thu, 2 Feb 2023 11:52:31 -0500 Subject: [PATCH 016/598] Feat: Add api endpoint to resolve youtube urls --- src/invidious/routes/api/v1/misc.cr | 27 +++++++++++++++++++++++++++ src/invidious/routing.cr | 1 + 2 files changed, 28 insertions(+) diff --git a/src/invidious/routes/api/v1/misc.cr b/src/invidious/routes/api/v1/misc.cr index 43d360e6..9679b530 100644 --- a/src/invidious/routes/api/v1/misc.cr +++ b/src/invidious/routes/api/v1/misc.cr @@ -150,4 +150,31 @@ module Invidious::Routes::API::V1::Misc response end + + # resolve channel and clip urls, return the UCID + def self.resolve_url(env) + env.response.content_type = "application/json" + url = env.params.query["url"]? + + return error_json(400, "Missing URL to resolve") if !url + + begin + resolved_url = YoutubeAPI.resolve_url(url.as(String)) + endpoint = resolved_url["endpoint"] + if resolved_ucid = endpoint.dig?("watchEndpoint", "videoId") + elsif resolved_ucid = endpoint.dig?("browseEndpoint", "browseId") + elsif pageType = endpoint.dig?("commandMetadata", "webCommandMetadata", "webPageType").try &.as_s || "" + if pageType == "WEB_PAGE_TYPE_UNKNOWN" + return error_json(400, "Unknown url") + end + end + rescue ex + return error_json(500, ex) + end + JSON.build do |json| + json.object do + json.field "ucid", resolved_ucid.try &.as_s || "" + end + end + end end diff --git a/src/invidious/routing.cr b/src/invidious/routing.cr index 157e6de7..fb9851a3 100644 --- a/src/invidious/routing.cr +++ b/src/invidious/routing.cr @@ -281,6 +281,7 @@ module Invidious::Routing get "/api/v1/playlists/:plid", {{namespace}}::Misc, :get_playlist get "/api/v1/auth/playlists/:plid", {{namespace}}::Misc, :get_playlist get "/api/v1/mixes/:rdid", {{namespace}}::Misc, :mixes + get "/api/v1/resolveurl", {{namespace}}::Misc, :resolve_url {% end %} end end From c162c7ff3f27498bd374b674bf7ca9b0c0790cc8 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Thu, 2 Feb 2023 18:20:14 -0500 Subject: [PATCH 017/598] add pageType --- src/invidious/routes/api/v1/misc.cr | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/invidious/routes/api/v1/misc.cr b/src/invidious/routes/api/v1/misc.cr index 9679b530..e499f4d6 100644 --- a/src/invidious/routes/api/v1/misc.cr +++ b/src/invidious/routes/api/v1/misc.cr @@ -161,12 +161,11 @@ module Invidious::Routes::API::V1::Misc begin resolved_url = YoutubeAPI.resolve_url(url.as(String)) endpoint = resolved_url["endpoint"] + pageType = endpoint.dig?("commandMetadata", "webCommandMetadata", "webPageType").try &.as_s || "" if resolved_ucid = endpoint.dig?("watchEndpoint", "videoId") elsif resolved_ucid = endpoint.dig?("browseEndpoint", "browseId") - elsif pageType = endpoint.dig?("commandMetadata", "webCommandMetadata", "webPageType").try &.as_s || "" - if pageType == "WEB_PAGE_TYPE_UNKNOWN" - return error_json(400, "Unknown url") - end + elsif pageType == "WEB_PAGE_TYPE_UNKNOWN" + return error_json(400, "Unknown url") end rescue ex return error_json(500, ex) @@ -174,6 +173,7 @@ module Invidious::Routes::API::V1::Misc JSON.build do |json| json.object do json.field "ucid", resolved_ucid.try &.as_s || "" + json.field "pageType", pageType end end end From b2589c74bedb73a1ab6e0afe1a921b97f80c4b8e Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Thu, 2 Feb 2023 19:14:02 -0500 Subject: [PATCH 018/598] Add API for import/export --- src/invidious/routes/api/v1/authenticated.cr | 49 ++++++++++++++++++++ src/invidious/routing.cr | 3 ++ 2 files changed, 52 insertions(+) diff --git a/src/invidious/routes/api/v1/authenticated.cr b/src/invidious/routes/api/v1/authenticated.cr index 421355bb..c6042e40 100644 --- a/src/invidious/routes/api/v1/authenticated.cr +++ b/src/invidious/routes/api/v1/authenticated.cr @@ -31,6 +31,55 @@ module Invidious::Routes::API::V1::Authenticated env.response.status_code = 204 end + def self.export_invidious(env) + env.response.content_type = "application/json" + user = env.get("user").as(User) + + playlists = Invidious::Database::Playlists.select_like_iv(user.email) + + return JSON.build do |json| + json.object do + json.field "subscriptions", user.subscriptions + json.field "watch_history", user.watched + json.field "preferences", user.preferences + json.field "playlists" do + json.array do + playlists.each do |playlist| + json.object do + json.field "title", playlist.title + json.field "description", html_to_content(playlist.description_html) + json.field "privacy", playlist.privacy.to_s + json.field "videos" do + json.array do + Invidious::Database::PlaylistVideos.select_ids(playlist.id, playlist.index, limit: 500).each do |video_id| + json.string video_id + end + end + end + end + end + end + end + end + end + end + + def self.import_invidious(env) + user = env.get("user").as(User) + + begin + if body = env.request.body + body = env.request.body.not_nil!.gets_to_end + else + body = "{}" + end + Invidious::User::Import.from_invidious(user, body) + rescue + end + + env.response.status_code = 204 + end + def self.feed(env) env.response.content_type = "application/json" diff --git a/src/invidious/routing.cr b/src/invidious/routing.cr index 157e6de7..d5766b90 100644 --- a/src/invidious/routing.cr +++ b/src/invidious/routing.cr @@ -254,6 +254,9 @@ module Invidious::Routing get "/api/v1/auth/preferences", {{namespace}}::Authenticated, :get_preferences post "/api/v1/auth/preferences", {{namespace}}::Authenticated, :set_preferences + get "/api/v1/auth/export/invidious", {{namespace}}::Authenticated, :export_invidious + post "/api/v1/auth/import/invidious", {{namespace}}::Authenticated, :import_invidious + get "/api/v1/auth/feed", {{namespace}}::Authenticated, :feed get "/api/v1/auth/subscriptions", {{namespace}}::Authenticated, :get_subscriptions From 2606decd21a84ac3cba914f327f60a8403398ed9 Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Sun, 5 Feb 2023 15:00:11 -0500 Subject: [PATCH 019/598] Refactor export function --- src/invidious/routes/api/v1/authenticated.cr | 28 +--------------- src/invidious/routes/subscriptions.cr | 26 +-------------- src/invidious/user/exports.cr | 35 ++++++++++++++++++++ 3 files changed, 37 insertions(+), 52 deletions(-) create mode 100644 src/invidious/user/exports.cr diff --git a/src/invidious/routes/api/v1/authenticated.cr b/src/invidious/routes/api/v1/authenticated.cr index c6042e40..6b935312 100644 --- a/src/invidious/routes/api/v1/authenticated.cr +++ b/src/invidious/routes/api/v1/authenticated.cr @@ -35,33 +35,7 @@ module Invidious::Routes::API::V1::Authenticated env.response.content_type = "application/json" user = env.get("user").as(User) - playlists = Invidious::Database::Playlists.select_like_iv(user.email) - - return JSON.build do |json| - json.object do - json.field "subscriptions", user.subscriptions - json.field "watch_history", user.watched - json.field "preferences", user.preferences - json.field "playlists" do - json.array do - playlists.each do |playlist| - json.object do - json.field "title", playlist.title - json.field "description", html_to_content(playlist.description_html) - json.field "privacy", playlist.privacy.to_s - json.field "videos" do - json.array do - Invidious::Database::PlaylistVideos.select_ids(playlist.id, playlist.index, limit: 500).each do |video_id| - json.string video_id - end - end - end - end - end - end - end - end - end + return Invidious::User::Export.to_invidious(user) end def self.import_invidious(env) diff --git a/src/invidious/routes/subscriptions.cr b/src/invidious/routes/subscriptions.cr index 7b1fa876..3090e026 100644 --- a/src/invidious/routes/subscriptions.cr +++ b/src/invidious/routes/subscriptions.cr @@ -106,31 +106,7 @@ module Invidious::Routes::Subscriptions env.response.headers["content-disposition"] = "attachment" playlists = Invidious::Database::Playlists.select_like_iv(user.email) - return JSON.build do |json| - json.object do - json.field "subscriptions", user.subscriptions - json.field "watch_history", user.watched - json.field "preferences", user.preferences - json.field "playlists" do - json.array do - playlists.each do |playlist| - json.object do - json.field "title", playlist.title - json.field "description", html_to_content(playlist.description_html) - json.field "privacy", playlist.privacy.to_s - json.field "videos" do - json.array do - Invidious::Database::PlaylistVideos.select_ids(playlist.id, playlist.index, limit: 500).each do |video_id| - json.string video_id - end - end - end - end - end - end - end - end - end + return Invidious::User::Export.to_invidious(user) else env.response.content_type = "application/xml" env.response.headers["content-disposition"] = "attachment" diff --git a/src/invidious/user/exports.cr b/src/invidious/user/exports.cr new file mode 100644 index 00000000..32be0ca2 --- /dev/null +++ b/src/invidious/user/exports.cr @@ -0,0 +1,35 @@ +struct Invidious::User + module Export + extend self + + def to_invidious(user : User) + playlists = Invidious::Database::Playlists.select_like_iv(user.email) + + return JSON.build do |json| + json.object do + json.field "subscriptions", user.subscriptions + json.field "watch_history", user.watched + json.field "preferences", user.preferences + json.field "playlists" do + json.array do + playlists.each do |playlist| + json.object do + json.field "title", playlist.title + json.field "description", html_to_content(playlist.description_html) + json.field "privacy", playlist.privacy.to_s + json.field "videos" do + json.array do + Invidious::Database::PlaylistVideos.select_ids(playlist.id, playlist.index, limit: 500).each do |video_id| + json.string video_id + end + end + end + end + end + end + end + end + end + end + end # module +end From 47a5b98e2554b32946864bc3320478d0dcc1daf8 Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Sun, 5 Feb 2023 15:43:58 -0500 Subject: [PATCH 020/598] Remove unused db call --- src/invidious/routes/subscriptions.cr | 1 - 1 file changed, 1 deletion(-) diff --git a/src/invidious/routes/subscriptions.cr b/src/invidious/routes/subscriptions.cr index 3090e026..0704c05e 100644 --- a/src/invidious/routes/subscriptions.cr +++ b/src/invidious/routes/subscriptions.cr @@ -104,7 +104,6 @@ module Invidious::Routes::Subscriptions if format == "json" env.response.content_type = "application/json" env.response.headers["content-disposition"] = "attachment" - playlists = Invidious::Database::Playlists.select_like_iv(user.email) return Invidious::User::Export.to_invidious(user) else From c37d8e36645d23afedff2729b8ad504cc5ba0655 Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Sun, 5 Feb 2023 15:49:56 -0500 Subject: [PATCH 021/598] Use CONFIG.playlist_length_limit when exporting playlists --- src/invidious/user/exports.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/user/exports.cr b/src/invidious/user/exports.cr index 32be0ca2..b52503c9 100644 --- a/src/invidious/user/exports.cr +++ b/src/invidious/user/exports.cr @@ -19,7 +19,7 @@ struct Invidious::User json.field "privacy", playlist.privacy.to_s json.field "videos" do json.array do - Invidious::Database::PlaylistVideos.select_ids(playlist.id, playlist.index, limit: 500).each do |video_id| + Invidious::Database::PlaylistVideos.select_ids(playlist.id, playlist.index, limit: CONFIG.playlist_length_limit).each do |video_id| json.string video_id end end From 28424d0e881c8595bbc5797b9ef46e98103fe6d6 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 7 Feb 2023 09:23:26 -0500 Subject: [PATCH 022/598] Ignore casing for trending type in api --- src/invidious/trending.cr | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/invidious/trending.cr b/src/invidious/trending.cr index 1f957081..d164c37f 100644 --- a/src/invidious/trending.cr +++ b/src/invidious/trending.cr @@ -4,11 +4,14 @@ def fetch_trending(trending_type, region, locale) plid = nil - if trending_type == "Music" + trending_type ||= "default" + trending_type = trending_type.downcase + + if trending_type == "music" params = "4gINGgt5dG1hX2NoYXJ0cw%3D%3D" - elsif trending_type == "Gaming" + elsif trending_type == "gaming" params = "4gIcGhpnYW1pbmdfY29ycHVzX21vc3RfcG9wdWxhcg%3D%3D" - elsif trending_type == "Movies" + elsif trending_type == "movies" params = "4gIKGgh0cmFpbGVycw%3D%3D" else # Default params = "" From d57d278f32e94d2bec75ffbc3c7bf28e6cb7638d Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Thu, 9 Feb 2023 15:00:23 -0500 Subject: [PATCH 023/598] Make itag optional under /latest_version --- src/invidious/routes/video_playback.cr | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/invidious/routes/video_playback.cr b/src/invidious/routes/video_playback.cr index 1e932d11..f24c0ded 100644 --- a/src/invidious/routes/video_playback.cr +++ b/src/invidious/routes/video_playback.cr @@ -256,7 +256,7 @@ module Invidious::Routes::VideoPlayback return error_template(400, "Invalid video ID") end - if itag.nil? || itag <= 0 || itag >= 1000 + if !itag.nil? && (itag <= 0 || itag >= 1000) return error_template(400, "Invalid itag") end @@ -277,7 +277,11 @@ module Invidious::Routes::VideoPlayback return error_template(500, ex) end - fmt = video.fmt_stream.find(nil) { |f| f["itag"].as_i == itag } || video.adaptive_fmts.find(nil) { |f| f["itag"].as_i == itag } + if itag.nil? + fmt = video.fmt_stream[-1] + else + fmt = video.fmt_stream.find(nil) { |f| f["itag"].as_i == itag } || video.adaptive_fmts.find(nil) { |f| f["itag"].as_i == itag } + end url = fmt.try &.["url"]?.try &.as_s if !url From 6f01d6eacf0719e8569a338e5a44615f159c5120 Mon Sep 17 00:00:00 2001 From: thtmnisamnstr Date: Fri, 10 Feb 2023 12:00:02 -0800 Subject: [PATCH 024/598] 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 838cbeffcc7c8f85c83f6ab3e97362f803bd766c Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Sat, 11 Feb 2023 08:41:26 -0500 Subject: [PATCH 025/598] Use case statement for trending_type Co-Authored-By: Samantaz Fox --- src/invidious/trending.cr | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/invidious/trending.cr b/src/invidious/trending.cr index d164c37f..134eb437 100644 --- a/src/invidious/trending.cr +++ b/src/invidious/trending.cr @@ -4,14 +4,12 @@ def fetch_trending(trending_type, region, locale) plid = nil - trending_type ||= "default" - trending_type = trending_type.downcase - - if trending_type == "music" + case trending_type.try &.downcase + when "music" params = "4gINGgt5dG1hX2NoYXJ0cw%3D%3D" - elsif trending_type == "gaming" + when "gaming" params = "4gIcGhpnYW1pbmdfY29ycHVzX21vc3RfcG9wdWxhcg%3D%3D" - elsif trending_type == "movies" + when "movies" params = "4gIKGgh0cmFpbGVycw%3D%3D" else # Default params = "" From 8384fa94c2de8c4bf561f4fe5964ce802f22a545 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 14 Feb 2023 22:48:37 -0500 Subject: [PATCH 026/598] Community: Parse polls --- src/invidious/channels/community.cr | 38 ++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/src/invidious/channels/community.cr b/src/invidious/channels/community.cr index 13af2d8b..a0e79c22 100644 --- a/src/invidious/channels/community.cr +++ b/src/invidious/channels/community.cr @@ -185,10 +185,40 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode) end end end - # TODO - # when .has_key?("pollRenderer") - # attachment = attachment["pollRenderer"] - # json.field "type", "poll" + when .has_key?("pollRenderer") + attachment = attachment["pollRenderer"] + json.field "type", "poll" + json.field "totalVotes", attachment["totalVotes"]["simpleText"].as_s + json.field "choices" do + json.array do + attachment["choices"].as_a.each do |choice| + json.object do + json.field "text", choice["text"]["runs"][0]["text"].as_s + # A choice can have an image associated with it. + # Ex post: https://www.youtube.com/post/UgkxD4XavXUD4NQiddJXXdohbwOwcVqrH9Re + if choice["image"]? + thumbnail = choice["image"]["thumbnails"][0].as_h + width = thumbnail["width"].as_i + height = thumbnail["height"].as_i + aspect_ratio = (width.to_f / height.to_f) + url = thumbnail["url"].as_s.gsub(/=w\d+-h\d+(-p)?(-nd)?(-df)?(-rwa)?/, "=s640") + qualities = {320, 560, 640, 1280, 2000} + json.field "image" do + json.array do + qualities.each do |quality| + json.object do + json.field "url", url.gsub(/=s\d+/, "=s#{quality}") + json.field "width", quality + json.field "height", (quality / aspect_ratio).ceil.to_i + end + end + end + end + end + end + end + end + end when .has_key?("postMultiImageRenderer") attachment = attachment["postMultiImageRenderer"] json.field "type", "multiImage" From aecbafbc7beb5c007031c02cfba9f419c58d4545 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 14 Feb 2023 22:52:59 -0500 Subject: [PATCH 027/598] Community: parse replyCount --- src/invidious/channels/community.cr | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/invidious/channels/community.cr b/src/invidious/channels/community.cr index a0e79c22..9f9f3fde 100644 --- a/src/invidious/channels/community.cr +++ b/src/invidious/channels/community.cr @@ -108,6 +108,8 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode) like_count = post["actionButtons"]["commentActionButtonsRenderer"]["likeButton"]["toggleButtonRenderer"]["accessibilityData"]["accessibilityData"]["label"] .try &.as_s.gsub(/\D/, "").to_i? || 0 + reply_count = short_text_to_number(post.dig?("actionButtons", "commentActionButtonsRenderer", "replyButton", "buttonRenderer", "text", "simpleText").try &.as_s || "0") + json.field "content", html_to_content(content_html) json.field "contentHtml", content_html @@ -115,6 +117,7 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode) json.field "publishedText", translate(locale, "`x` ago", recode_date(published, locale)) json.field "likeCount", like_count + json.field "replyCount", reply_count json.field "commentId", post["postId"]? || post["commentId"]? || "" json.field "authorIsChannelOwner", post["authorEndpoint"]["browseEndpoint"]["browseId"] == ucid From 4731480821247a542ff05a8faedefcef55c009d9 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 14 Feb 2023 23:03:25 -0500 Subject: [PATCH 028/598] parse votes as number Co-Authored-By: syeopite <70992037+syeopite@users.noreply.github.com> --- src/invidious/channels/community.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/channels/community.cr b/src/invidious/channels/community.cr index 9f9f3fde..87659c47 100644 --- a/src/invidious/channels/community.cr +++ b/src/invidious/channels/community.cr @@ -191,7 +191,7 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode) when .has_key?("pollRenderer") attachment = attachment["pollRenderer"] json.field "type", "poll" - json.field "totalVotes", attachment["totalVotes"]["simpleText"].as_s + json.field "totalVotes", short_text_to_number(attachment["totalVotes"]["simpleText"].as_s.split(" ")[0]) json.field "choices" do json.array do attachment["choices"].as_a.each do |choice| From d03a62641f20a8dfd15fc9fe50373a5e75ee3d6e Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Wed, 15 Feb 2023 00:20:45 -0500 Subject: [PATCH 029/598] Add support for custom emojis in comments --- src/invidious/comments.cr | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr index 357a461c..5749248e 100644 --- a/src/invidious/comments.cr +++ b/src/invidious/comments.cr @@ -182,7 +182,11 @@ def fetch_youtube_comments(id, cursor, format, locale, thin_mode, region, sort_b json.field "contentHtml", content_html json.field "isPinned", (node_comment["pinnedCommentBadge"]? != nil) - + json.field "isMember", (node_comment["sponsorCommentBadge"]? != nil) + if node_comment["sponsorCommentBadge"]? + # Member icon thumbnails always have one object and there's only ever the url property in it + json.field "memberIconUrl", node_comment["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)) @@ -674,6 +678,14 @@ def content_to_comment_html(content, video_id : String? = "") text = "#{text}" if run["bold"]? text = "#{text}" if run["strikethrough"]? text = "#{text}" if run["italics"]? + if emojiImage = run.dig?("emoji", "image") + emojiAlt = emojiImage.dig?("accessibility", "accessibilityData", "label").try &.as_s || text + emojiThumb = emojiImage["thumbnails"][0] + emojiUrl = "/ggpht#{URI.parse(emojiThumb["url"].as_s).request_target}" + emojiWidth = emojiThumb["width"] + emojiHeight = emojiThumb["height"] + text = "\"#{emojiAlt}\"" + end text end From 76ad4e802603f82fe45d522a9c268e972d428a75 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Thu, 16 Feb 2023 14:12:56 -0500 Subject: [PATCH 030/598] show member icon, hide deleted emojis, fix non-custom emojis --- locales/en-US.json | 1 + src/invidious/comments.cr | 29 ++++++++++++++++++++++------- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/locales/en-US.json b/locales/en-US.json index a5c16fd7..5bbf6db6 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -405,6 +405,7 @@ "YouTube comment permalink": "YouTube comment permalink", "permalink": "permalink", "`x` marked it with a ❤": "`x` marked it with a ❤", + "Member": "Member", "Audio mode": "Audio mode", "Video mode": "Video mode", "Playlists": "Playlists", diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr index 5749248e..f1942ceb 100644 --- a/src/invidious/comments.cr +++ b/src/invidious/comments.cr @@ -328,11 +328,21 @@ def template_youtube_comments(comments, locale, thin_mode, is_replies = false) end author_name = HTML.escape(child["author"].as_s) + member_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["isMember"]?.try &.as_bool + member_icon = "\"\"" + end html << <<-END_HTML
@@ -343,6 +353,7 @@ def template_youtube_comments(comments, locale, thin_mode, is_replies = false) #{author_name} + #{member_icon}

#{child["contentHtml"]}

END_HTML @@ -678,13 +689,17 @@ def content_to_comment_html(content, video_id : String? = "") text = "#{text}" if run["bold"]? text = "#{text}" if run["strikethrough"]? text = "#{text}" if run["italics"]? - if emojiImage = run.dig?("emoji", "image") - emojiAlt = emojiImage.dig?("accessibility", "accessibilityData", "label").try &.as_s || text - emojiThumb = emojiImage["thumbnails"][0] - emojiUrl = "/ggpht#{URI.parse(emojiThumb["url"].as_s).request_target}" - emojiWidth = emojiThumb["width"] - emojiHeight = emojiThumb["height"] - text = "\"#{emojiAlt}\"" + if run["emoji"]? + if run["emoji"]["isCustomEmoji"]?.try &.as_bool + if emojiImage = run.dig?("emoji", "image") + emojiAlt = emojiImage.dig?("accessibility", "accessibilityData", "label").try &.as_s || text + emojiThumb = emojiImage["thumbnails"][0] + text = "\"#{emojiAlt}\"" + else + # Hide deleted channel emoji + text = "" + end + end end text From a95f82e44bfc77ac8fc1b7acd2159d185a4fa637 Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Fri, 17 Feb 2023 12:08:05 -0500 Subject: [PATCH 031/598] Add Playlet to "Projects using Invidious" (#3640) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 8d668a29..0744ac50 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,7 @@ Weblate also allows you to log-in with major SSO providers like Github, Gitlab, - [Yattee](https://github.com/yattee/yattee): Alternative YouTube frontend for iPhone, iPad, Mac and Apple TV. - [TubiTui](https://codeberg.org/777/TubiTui): A lightweight, libre, TUI-based YouTube client. - [Ytfzf](https://github.com/pystardust/ytfzf): A posix script to find and watch youtube videos from the terminal. (Without API) +- [Playlet](https://github.com/iBicha/playlet): Unofficial Youtube client for Roku TV ## Liability From bc5d81fe60b324459ac428f4269316bd4cfdc3a1 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Sun, 19 Feb 2023 12:46:46 -0500 Subject: [PATCH 032/598] use string builder to create images change member to sponsor --- locales/en-US.json | 2 +- src/invidious/comments.cr | 36 ++++++++++++++++++++++-------------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/locales/en-US.json b/locales/en-US.json index 5bbf6db6..bd2b9d44 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -405,7 +405,7 @@ "YouTube comment permalink": "YouTube comment permalink", "permalink": "permalink", "`x` marked it with a ❤": "`x` marked it with a ❤", - "Member": "Member", + "Channel Sponsor": "Channel Sponsor", "Audio mode": "Audio mode", "Video mode": "Video mode", "Playlists": "Playlists", diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr index f1942ceb..b866b6ef 100644 --- a/src/invidious/comments.cr +++ b/src/invidious/comments.cr @@ -182,10 +182,10 @@ def fetch_youtube_comments(id, cursor, format, locale, thin_mode, region, sort_b json.field "contentHtml", content_html json.field "isPinned", (node_comment["pinnedCommentBadge"]? != nil) - json.field "isMember", (node_comment["sponsorCommentBadge"]? != nil) + json.field "isSponsor", (node_comment["sponsorCommentBadge"]? != nil) if node_comment["sponsorCommentBadge"]? - # Member icon thumbnails always have one object and there's only ever the url property in it - json.field "memberIconUrl", node_comment["sponsorCommentBadge"]["sponsorCommentBadgeRenderer"]["customBadge"]["thumbnails"][0]["url"].to_s + # 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)) @@ -328,20 +328,19 @@ def template_youtube_comments(comments, locale, thin_mode, is_replies = false) end author_name = HTML.escape(child["author"].as_s) - member_icon = "" + 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["isMember"]?.try &.as_bool - member_icon = "\"\"" + if child["isSponsor"].as_bool + sponsor_icon = String.build do |str| + str << %() + end end html << <<-END_HTML
@@ -353,7 +352,7 @@ def template_youtube_comments(comments, locale, thin_mode, is_replies = false) #{author_name} - #{member_icon} + #{sponsor_icon}

#{child["contentHtml"]}

END_HTML @@ -689,12 +688,21 @@ def content_to_comment_html(content, video_id : String? = "") text = "#{text}" if run["bold"]? text = "#{text}" if run["strikethrough"]? text = "#{text}" if run["italics"]? + + # check for custom emojis if run["emoji"]? if run["emoji"]["isCustomEmoji"]?.try &.as_bool if emojiImage = run.dig?("emoji", "image") emojiAlt = emojiImage.dig?("accessibility", "accessibilityData", "label").try &.as_s || text emojiThumb = emojiImage["thumbnails"][0] - text = "\"#{emojiAlt}\"" + text = String.build do |str| + str << %() << emojiAlt << ') + end else # Hide deleted channel emoji text = "" From b287ddc52acf43c3d3a5fc11e42a8b1b8d66e800 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89milien=20Devos=20=28perso=29?= Date: Sun, 19 Feb 2023 20:20:47 +0100 Subject: [PATCH 033/598] Allow to set a label for exempting from staling (#3651) --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 11168aea..a945da58 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -23,4 +23,4 @@ jobs: stale-pr-label: "stale" ascending: true # Never mark feature requests/enhancements as stale - exempt-issue-labels: "feature-request,enhancement" + exempt-issue-labels: "feature-request,enhancement,exempt-stale" From bde21d527f1fae4a84b964f1b297d7b246526ba0 Mon Sep 17 00:00:00 2001 From: Wes van der Vleuten <16665772+WesVleuten@users.noreply.github.com> Date: Sun, 19 Feb 2023 20:41:18 +0100 Subject: [PATCH 034/598] Fixed console error --- assets/js/watched_indicator.js | 24 +++++++++++++++++++++ assets/js/watched_widget.js | 24 --------------------- src/invidious/views/add_playlist_items.ecr | 2 +- src/invidious/views/channel.ecr | 2 +- src/invidious/views/edit_playlist.ecr | 2 +- src/invidious/views/feeds/playlists.ecr | 2 +- src/invidious/views/feeds/popular.ecr | 2 +- src/invidious/views/feeds/subscriptions.ecr | 3 ++- src/invidious/views/feeds/trending.ecr | 2 +- src/invidious/views/hashtag.ecr | 2 +- src/invidious/views/playlist.ecr | 2 +- src/invidious/views/search.ecr | 2 +- 12 files changed, 35 insertions(+), 34 deletions(-) create mode 100644 assets/js/watched_indicator.js diff --git a/assets/js/watched_indicator.js b/assets/js/watched_indicator.js new file mode 100644 index 00000000..e971cd80 --- /dev/null +++ b/assets/js/watched_indicator.js @@ -0,0 +1,24 @@ +'use strict'; +var save_player_pos_key = 'save_player_pos'; + +function get_all_video_times() { + return helpers.storage.get(save_player_pos_key) || {}; +} + +document.querySelectorAll('.watched-indicator').forEach(function (indicator) { + var watched_part = get_all_video_times()[indicator.dataset.id]; + var total = parseInt(indicator.dataset.length, 10); + if (watched_part === undefined) { + watched_part = total; + } + var percentage = Math.round((watched_part / total) * 100); + + if (percentage < 5) { + percentage = 5; + } + if (percentage > 90) { + percentage = 100; + } + + indicator.style.width = percentage + '%'; +}); diff --git a/assets/js/watched_widget.js b/assets/js/watched_widget.js index 02537111..f1ac9cb4 100644 --- a/assets/js/watched_widget.js +++ b/assets/js/watched_widget.js @@ -32,27 +32,3 @@ function mark_unwatched(target) { } }); } - -var save_player_pos_key = 'save_player_pos'; - -function get_all_video_times() { - return helpers.storage.get(save_player_pos_key) || {}; -} - -document.querySelectorAll('.watched-indicator').forEach(function (indicator) { - var watched_part = get_all_video_times()[indicator.dataset.id]; - var total = parseInt(indicator.dataset.length, 10); - if (watched_part === undefined) { - watched_part = total; - } - var percentage = Math.round((watched_part / total) * 100); - - if (percentage < 5) { - percentage = 5; - } - if (percentage > 90) { - percentage = 100; - } - - indicator.style.width = percentage + '%'; -}); diff --git a/src/invidious/views/add_playlist_items.ecr b/src/invidious/views/add_playlist_items.ecr index 70575de3..bcba74cf 100644 --- a/src/invidious/views/add_playlist_items.ecr +++ b/src/invidious/views/add_playlist_items.ecr @@ -39,7 +39,7 @@ <% end %>
- + <% if query %> <%- query_encoded = URI.encode_www_form(query.text, space_to_plus: true) -%> diff --git a/src/invidious/views/channel.ecr b/src/invidious/views/channel.ecr index 931dd407..6e62a471 100644 --- a/src/invidious/views/channel.ecr +++ b/src/invidious/views/channel.ecr @@ -49,7 +49,7 @@ <% end %>
- +
diff --git a/src/invidious/views/edit_playlist.ecr b/src/invidious/views/edit_playlist.ecr index 100764c7..548104c8 100644 --- a/src/invidious/views/edit_playlist.ecr +++ b/src/invidious/views/edit_playlist.ecr @@ -62,7 +62,7 @@ <% end %>
- +
diff --git a/src/invidious/views/feeds/playlists.ecr b/src/invidious/views/feeds/playlists.ecr index f9064762..e52a7707 100644 --- a/src/invidious/views/feeds/playlists.ecr +++ b/src/invidious/views/feeds/playlists.ecr @@ -33,4 +33,4 @@ <% end %>
- + diff --git a/src/invidious/views/feeds/popular.ecr b/src/invidious/views/feeds/popular.ecr index 919002cd..5fbe539c 100644 --- a/src/invidious/views/feeds/popular.ecr +++ b/src/invidious/views/feeds/popular.ecr @@ -17,4 +17,4 @@ <% end %>
- + diff --git a/src/invidious/views/feeds/subscriptions.ecr b/src/invidious/views/feeds/subscriptions.ecr index d4e93240..9c69c5b0 100644 --- a/src/invidious/views/feeds/subscriptions.ecr +++ b/src/invidious/views/feeds/subscriptions.ecr @@ -54,6 +54,7 @@ }.to_pretty_json %> +
<% videos.each do |item| %> @@ -61,7 +62,7 @@ <% end %>
- +
diff --git a/src/invidious/views/feeds/trending.ecr b/src/invidious/views/feeds/trending.ecr index 76218165..7dc416c6 100644 --- a/src/invidious/views/feeds/trending.ecr +++ b/src/invidious/views/feeds/trending.ecr @@ -46,4 +46,4 @@ <% end %>
- + diff --git a/src/invidious/views/hashtag.ecr b/src/invidious/views/hashtag.ecr index 6064af74..3351c21c 100644 --- a/src/invidious/views/hashtag.ecr +++ b/src/invidious/views/hashtag.ecr @@ -24,7 +24,7 @@ <%- end -%>
- +
diff --git a/src/invidious/views/playlist.ecr b/src/invidious/views/playlist.ecr index 1df047ba..a04acf4c 100644 --- a/src/invidious/views/playlist.ecr +++ b/src/invidious/views/playlist.ecr @@ -106,7 +106,7 @@ <% end %>
- +
diff --git a/src/invidious/views/search.ecr b/src/invidious/views/search.ecr index c4960d08..a7469e36 100644 --- a/src/invidious/views/search.ecr +++ b/src/invidious/views/search.ecr @@ -37,7 +37,7 @@
<%- end -%> - +
From b5eb6016bbc455921ce3d8ec24589d706f8a5fb1 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Sun, 19 Feb 2023 14:51:39 -0500 Subject: [PATCH 035/598] add spaces at end of attribute --- src/invidious/comments.cr | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr index b866b6ef..6c323bc1 100644 --- a/src/invidious/comments.cr +++ b/src/invidious/comments.cr @@ -336,10 +336,10 @@ def template_youtube_comments(comments, locale, thin_mode, is_replies = false) end if child["isSponsor"].as_bool sponsor_icon = String.build do |str| - str << %() + str << %() end end html << <<-END_HTML @@ -696,12 +696,12 @@ def content_to_comment_html(content, video_id : String? = "") emojiAlt = emojiImage.dig?("accessibility", "accessibilityData", "label").try &.as_s || text emojiThumb = emojiImage["thumbnails"][0] text = String.build do |str| - str << %() << emojiAlt << ') + str << %() << emojiAlt << ) end else # Hide deleted channel emoji From 8046316f200801e2e8c34ce2d43da6a16fb86fe8 Mon Sep 17 00:00:00 2001 From: Raman Date: Tue, 14 Feb 2023 07:26:13 +0000 Subject: [PATCH 036/598] Update Hindi translation --- locales/hi.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/locales/hi.json b/locales/hi.json index e576080f..54e0fe84 100644 --- a/locales/hi.json +++ b/locales/hi.json @@ -470,5 +470,7 @@ "crash_page_switch_instance": "किसी दूसरे उदाहरण का इस्तेमाल करें", "crash_page_read_the_faq": "अक्सर पूछे जाने वाले प्रश्न (FAQ) पढ़ें", "crash_page_refresh": "पृष्ठ को एक बार साफ़ करें", - "crash_page_search_issue": "GitHub पर मौजूदा मुद्दे ढूँढ़ें" + "crash_page_search_issue": "GitHub पर मौजूदा मुद्दे ढूँढ़ें", + "Popular enabled: ": "लोकप्रिय सक्षम: ", + "Artist: ": "कलाकार: " } From 64780ce1da79ce5ea7f1619b46cb9e7137c4cc97 Mon Sep 17 00:00:00 2001 From: Andrey Date: Fri, 17 Feb 2023 20:47:00 +0000 Subject: [PATCH 037/598] Update Russian translation --- locales/ru.json | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/locales/ru.json b/locales/ru.json index 733e0be1..7ca5cf1f 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -69,11 +69,11 @@ "preferences_vr_mode_label": "Интерактивные 360-градусные видео (необходим WebGL): ", "preferences_category_visual": "Настройки сайта", "preferences_player_style_label": "Стиль проигрывателя: ", - "Dark mode: ": "Тёмное оформление: ", + "Dark mode: ": "Темное оформление: ", "preferences_dark_mode_label": "Тема: ", - "dark": "тёмная", + "dark": "темная", "light": "светлая", - "preferences_thin_mode_label": "Облегчённое оформление: ", + "preferences_thin_mode_label": "Облегченное оформление: ", "preferences_category_misc": "Прочие настройки", "preferences_automatic_instance_redirect_label": "Автоматическая смена зеркала (переход на redirect.invidious.io): ", "preferences_category_subscription": "Настройки подписок", @@ -88,7 +88,7 @@ "channel name": "по названию канала", "channel name - reverse": "по названию канала в обратном порядке", "Only show latest video from channel: ": "Показывать только последние видео с каналов: ", - "Only show latest unwatched video from channel: ": "Показывать только непросмотренные видео с каналов: ", + "Only show latest unwatched video from channel: ": "Показывать только последние непросмотренные видео с канала: ", "preferences_unseen_only_label": "Показывать только непросмотренные видео: ", "preferences_notifications_only_label": "Показывать только оповещения, если они есть: ", "Enable web notifications": "Включить уведомления в браузере", @@ -147,13 +147,13 @@ "License: ": "Лицензия: ", "Family friendly? ": "Семейный просмотр: ", "Wilson score: ": "Оценка Уилсона: ", - "Engagement: ": "Вовлечённость: ", + "Engagement: ": "Вовлеченность: ", "Whitelisted regions: ": "Доступно в регионах: ", "Blacklisted regions: ": "Недоступно в регионах: ", "Shared `x`": "Опубликовано `x`", "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. Нажмите сюда, чтобы увидеть комментарии. Но учтите: они могут загружаться немного медленнее.", + "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Похоже, у вас отключен JavaScript. Нажмите сюда, чтобы увидеть комментарии. Но учтите: они могут загружаться немного медленнее.", "View YouTube comments": "Показать комментарии с YouTube", "View more comments on Reddit": "Посмотреть больше комментариев на Reddit", "View `x` comments": { @@ -180,23 +180,23 @@ "Please log in": "Пожалуйста, войдите", "Invidious Private Feed for `x`": "Приватная лента Invidious для `x`", "channel:`x`": "канал: `x`", - "Deleted or invalid channel": "Канал удалён или не найден", + "Deleted or invalid channel": "Канал удален или не найден", "This channel does not exist.": "Такого канала не существует.", - "Could not get channel info.": "Не удаётся получить информацию об этом канале.", - "Could not fetch comments": "Не удаётся загрузить комментарии", + "Could not get channel info.": "Не удается получить информацию об этом канале.", + "Could not fetch comments": "Не удается загрузить комментарии", "`x` ago": "`x` назад", - "Load more": "Загрузить ещё", + "Load more": "Загрузить еще", "Could not create mix.": "Не удалось создать микс.", "Empty playlist": "Плейлист пуст", - "Not a playlist.": "Некорректный плейлист.", + "Not a playlist.": "Это не плейлист.", "Playlist does not exist.": "Плейлист не существует.", - "Could not pull trending pages.": "Не удаётся загрузить страницы «в тренде».", + "Could not pull trending pages.": "Не удается загрузить страницы «в тренде».", "Hidden field \"challenge\" is a required field": "Необходимо заполнить скрытое поле «challenge»", "Hidden field \"token\" is a required field": "Необходимо заполнить скрытое поле «токен»", "Erroneous challenge": "Неправильный ответ в «challenge»", "Erroneous token": "Неправильный токен", "No such user": "Пользователь не найден", - "Token is expired, please try again": "Срок действия токена истёк, попробуйте позже", + "Token is expired, please try again": "Срок действия токена истек, попробуйте позже", "English": "Английский", "English (auto-generated)": "Английский (созданы автоматически)", "Afrikaans": "Африкаанс", @@ -453,8 +453,8 @@ "Portuguese (Brazil)": "Португальский (Бразилия)", "footer_source_code": "Исходный код", "footer_original_source_code": "Оригинальный исходный код", - "footer_modfied_source_code": "Изменённый исходный код", - "user_saved_playlists": "`x` сохранённых плейлистов", + "footer_modfied_source_code": "Измененный исходный код", + "user_saved_playlists": "`x` сохраненных плейлистов", "crash_page_search_issue": "поискали похожую проблему на GitHub", "comments_points_count_0": "{{count}} плюс", "comments_points_count_1": "{{count}} плюса", From 8445d3ae120c52eba531183caa1fa63d5701f322 Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Sun, 19 Feb 2023 19:01:28 -0500 Subject: [PATCH 038/598] Fix watch history order --- src/invidious/database/users.cr | 1 + src/invidious/routes/watch.cr | 6 ++---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/invidious/database/users.cr b/src/invidious/database/users.cr index 0a4a4fd8..f8422874 100644 --- a/src/invidious/database/users.cr +++ b/src/invidious/database/users.cr @@ -50,6 +50,7 @@ module Invidious::Database::Users end def mark_watched(user : User, vid : String) + mark_unwatched(user, vid) request = <<-SQL UPDATE users SET watched = array_append(watched, $1) diff --git a/src/invidious/routes/watch.cr b/src/invidious/routes/watch.cr index 5d3845c3..813cb0f4 100644 --- a/src/invidious/routes/watch.cr +++ b/src/invidious/routes/watch.cr @@ -76,7 +76,7 @@ module Invidious::Routes::Watch end env.params.query.delete_all("iv_load_policy") - if watched && preferences.watch_history && !watched.includes? id + if watched && preferences.watch_history Invidious::Database::Users.mark_watched(user.as(User), id) end @@ -259,9 +259,7 @@ module Invidious::Routes::Watch case action when "action_mark_watched" - if !user.watched.includes? id - Invidious::Database::Users.mark_watched(user, id) - end + Invidious::Database::Users.mark_watched(user, id) when "action_mark_unwatched" Invidious::Database::Users.mark_unwatched(user, id) else From 20289a4d014d36c9a7bd50d8b1549bf36f78eb59 Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Mon, 20 Feb 2023 14:56:38 -0500 Subject: [PATCH 039/598] Fix order for import --- src/invidious/user/imports.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/user/imports.cr b/src/invidious/user/imports.cr index 20ae0d47..aa947456 100644 --- a/src/invidious/user/imports.cr +++ b/src/invidious/user/imports.cr @@ -48,7 +48,7 @@ struct Invidious::User if data["watch_history"]? user.watched += data["watch_history"].as_a.map(&.as_s) - user.watched.uniq! + user.watched.reverse!.uniq!.reverse! Invidious::Database::Users.update_watch_history(user) end From 7b124eec640ca601d2cafc366867e1d6cd283577 Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Mon, 20 Feb 2023 16:27:16 -0500 Subject: [PATCH 040/598] Add History API --- src/invidious/routes/api/v1/authenticated.cr | 50 ++++++++++++++++++++ src/invidious/routing.cr | 5 ++ 2 files changed, 55 insertions(+) diff --git a/src/invidious/routes/api/v1/authenticated.cr b/src/invidious/routes/api/v1/authenticated.cr index 6b935312..e670a87c 100644 --- a/src/invidious/routes/api/v1/authenticated.cr +++ b/src/invidious/routes/api/v1/authenticated.cr @@ -54,6 +54,56 @@ module Invidious::Routes::API::V1::Authenticated env.response.status_code = 204 end + def self.get_history(env) + env.response.content_type = "application/json" + user = env.get("user").as(User) + + page = env.params.query["page"]?.try &.to_i? + page ||= 1 + + max_results = env.params.query["max_results"]?.try &.to_i?.try &.clamp(0, MAX_ITEMS_PER_PAGE) + max_results ||= user.preferences.max_results + max_results ||= CONFIG.default_user_preferences.max_results + + if user.watched[(page - 1) * max_results]? + watched = user.watched.reverse[(page - 1) * max_results, max_results] + end + watched ||= [] of String + + return watched.to_json + end + + def self.mark_watched(env) + user = env.get("user").as(User) + + id = env.params.url["id"]?.try &.as(String) + if !id + return error_json(400, "Invalid video id.") + end + + Invidious::Database::Users.mark_watched(user, id) + env.response.status_code = 204 + end + + def self.mark_unwatched(env) + user = env.get("user").as(User) + + id = env.params.url["id"]?.try &.as(String) + if !id + return error_json(400, "Invalid video id.") + end + + Invidious::Database::Users.mark_unwatched(user, id) + env.response.status_code = 204 + end + + def self.clear_history(env) + user = env.get("user").as(User) + + Invidious::Database::Users.clear_watch_history(user) + env.response.status_code = 204 + end + def self.feed(env) env.response.content_type = "application/json" diff --git a/src/invidious/routing.cr b/src/invidious/routing.cr index dca2f117..9e2ade3d 100644 --- a/src/invidious/routing.cr +++ b/src/invidious/routing.cr @@ -257,6 +257,11 @@ module Invidious::Routing get "/api/v1/auth/export/invidious", {{namespace}}::Authenticated, :export_invidious post "/api/v1/auth/import/invidious", {{namespace}}::Authenticated, :import_invidious + get "/api/v1/auth/history", {{namespace}}::Authenticated, :get_history + post "/api/v1/auth/history/:id", {{namespace}}::Authenticated, :mark_watched + delete "/api/v1/auth/history/:id", {{namespace}}::Authenticated, :mark_unwatched + delete "/api/v1/auth/history", {{namespace}}::Authenticated, :clear_history + get "/api/v1/auth/feed", {{namespace}}::Authenticated, :feed get "/api/v1/auth/subscriptions", {{namespace}}::Authenticated, :get_subscriptions From 15e9510ab212ac1f8b6bc2a5a3e83ebc4ba1fe90 Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Mon, 20 Feb 2023 16:43:36 -0500 Subject: [PATCH 041/598] Check preferences before marking video as watched --- src/invidious/routes/api/v1/authenticated.cr | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/invidious/routes/api/v1/authenticated.cr b/src/invidious/routes/api/v1/authenticated.cr index e670a87c..dc86bb3c 100644 --- a/src/invidious/routes/api/v1/authenticated.cr +++ b/src/invidious/routes/api/v1/authenticated.cr @@ -76,6 +76,10 @@ module Invidious::Routes::API::V1::Authenticated def self.mark_watched(env) user = env.get("user").as(User) + if !user.preferences.watch_history + return error_json(409, "Watch history is disabled in preferences.") + end + id = env.params.url["id"]?.try &.as(String) if !id return error_json(400, "Invalid video id.") From 6ee51f460a27618d5926e9caf230a7ada2823e70 Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Tue, 21 Feb 2023 15:24:25 -0500 Subject: [PATCH 042/598] encode username on callback --- src/invidious/routes/account.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/routes/account.cr b/src/invidious/routes/account.cr index d01aee56..284d5b06 100644 --- a/src/invidious/routes/account.cr +++ b/src/invidious/routes/account.cr @@ -262,7 +262,7 @@ module Invidious::Routes::Account end query["token"] = access_token - query["username"] = user.email + query["username"] = URI.encode_path_segment(user.email) url.query = query.to_s env.redirect url.to_s From 57e4312d9fabf4dc284426c74db952c3609f9987 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Marcelo=20Alvarenga?= Date: Mon, 20 Feb 2023 22:56:13 +0000 Subject: [PATCH 043/598] Update Portuguese (Brazil) translation --- locales/pt-BR.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/locales/pt-BR.json b/locales/pt-BR.json index afd31ede..079c4ea1 100644 --- a/locales/pt-BR.json +++ b/locales/pt-BR.json @@ -476,5 +476,8 @@ "channel_tab_channels_label": "Canais", "channel_tab_playlists_label": "Listas de reprodução", "channel_tab_shorts_label": "Curtos", - "channel_tab_streams_label": "Ao Vivo" + "channel_tab_streams_label": "Ao Vivo", + "Music in this video": "Música neste vídeo", + "Artist: ": "Artista: ", + "Album: ": "Álbum: " } From 596a16c085c6e3afd998273ab3c9bff3c109e07c Mon Sep 17 00:00:00 2001 From: ssantos Date: Mon, 20 Feb 2023 14:05:29 +0000 Subject: [PATCH 044/598] Update Portuguese (Portugal) translation --- locales/pt-PT.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/locales/pt-PT.json b/locales/pt-PT.json index 1788deb1..43834d70 100644 --- a/locales/pt-PT.json +++ b/locales/pt-PT.json @@ -472,5 +472,12 @@ "search_message_change_filters_or_query": "Tente alargar os termos genéricos da pesquisa e/ou alterar os filtros.", "crash_page_refresh": "tentou recarregar a página", "crash_page_switch_instance": "tentou usar outra instância", - "error_video_not_in_playlist": "O vídeo pedido não existe nesta lista de reprodução. Clique aqui para a página inicial da lista de reprodução." + "error_video_not_in_playlist": "O vídeo pedido não existe nesta lista de reprodução. Clique aqui para a página inicial da lista de reprodução.", + "Artist: ": "Artista: ", + "Album: ": "Álbum: ", + "channel_tab_streams_label": "Diretos", + "channel_tab_playlists_label": "Listas de reprodução", + "channel_tab_channels_label": "Canais", + "Music in this video": "Música neste vídeo", + "channel_tab_shorts_label": "Curtos" } From 7e0210d090b0b6141832944bba19ca9fe1170817 Mon Sep 17 00:00:00 2001 From: Saurmandal Date: Mon, 20 Feb 2023 15:39:25 +0000 Subject: [PATCH 045/598] Update Hindi translation --- locales/hi.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/locales/hi.json b/locales/hi.json index 54e0fe84..41335266 100644 --- a/locales/hi.json +++ b/locales/hi.json @@ -472,5 +472,12 @@ "crash_page_refresh": "पृष्ठ को एक बार साफ़ करें", "crash_page_search_issue": "GitHub पर मौजूदा मुद्दे ढूँढ़ें", "Popular enabled: ": "लोकप्रिय सक्षम: ", - "Artist: ": "कलाकार: " + "Artist: ": "कलाकार: ", + "Music in this video": "इस वीडियो में संगीत", + "Album: ": "एल्बम: ", + "error_video_not_in_playlist": "अनुरोधित वीडियो इस प्लेलिस्ट में मौजूद नहीं है। प्लेलिस्ट के मुखपृष्ठ पर जाने के लिए यहाँ क्लिक करें।", + "channel_tab_shorts_label": "शॉर्ट्स", + "channel_tab_streams_label": "लाइवस्ट्रीम्स", + "channel_tab_playlists_label": "प्लेलिस्ट्स", + "channel_tab_channels_label": "चैनल्स" } From b3eea6ab3ebdb1618916b02041b22e0e238e8a7d Mon Sep 17 00:00:00 2001 From: thtmnisamnstr Date: Thu, 23 Feb 2023 15:55:38 -0800 Subject: [PATCH 046/598] 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 8eca5b270ed10b6233371f5495cf059bc353dcb1 Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Sat, 14 Jan 2023 01:49:58 +0100 Subject: [PATCH 047/598] Video: Fix 0 views, and empty license field --- locales/en-US.json | 1 + src/invidious/videos/parser.cr | 2 +- src/invidious/views/watch.ecr | 6 +++++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/locales/en-US.json b/locales/en-US.json index a5c16fd7..fbcc1341 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -183,6 +183,7 @@ "Show annotations": "Show annotations", "Genre: ": "Genre: ", "License: ": "License: ", + "Standard YouTube license": "Standard YouTube license", "Family friendly? ": "Family friendly? ", "Wilson score: ": "Wilson score: ", "Engagement: ": "Engagement: ", diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr index cf43f1be..04ee7303 100644 --- a/src/invidious/videos/parser.cr +++ b/src/invidious/videos/parser.cr @@ -186,7 +186,7 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any # then from videoDetails, as the latter is "0" for livestreams (we want # to get the amount of viewers watching). views_txt = video_primary_renderer - .try &.dig?("viewCount", "videoViewCountRenderer", "viewCount", "runs", 0, "text") + .try &.dig?("viewCount", "videoViewCountRenderer", "viewCount", "simpleText") views_txt ||= video_details["viewCount"]? views = views_txt.try &.as_s.gsub(/\D/, "").to_i64? diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 666eb3b0..c23a9552 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -181,7 +181,11 @@ we're going to need to do it here in order to allow for translations. <% end %>

<% if video.license %> -

<%= translate(locale, "License: ") %><%= video.license %>

+ <% if video.license == "" %> +

<%= translate(locale, "License: ") %><%= translate(locale, "Standard YouTube license") %>

+ <% else %> +

<%= translate(locale, "License: ") %><%= video.license %>

+ <% end %> <% end %>

<%= translate(locale, "Family friendly? ") %><%= translate_bool(locale, video.is_family_friendly) %>

From 4ac263f1dfdc18e5de584d4fb8bbfd74141e2716 Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Sun, 15 Jan 2023 16:26:51 +0100 Subject: [PATCH 048/598] Replace == with empty? --- src/invidious/views/watch.ecr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index c23a9552..1fc79495 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -181,7 +181,7 @@ we're going to need to do it here in order to allow for translations. <% end %>

<% if video.license %> - <% if video.license == "" %> + <% if video.license.empty? %>

<%= translate(locale, "License: ") %><%= translate(locale, "Standard YouTube license") %>

<% else %>

<%= translate(locale, "License: ") %><%= video.license %>

From 3ddcfea8faea4d6a6e4db9c52b2c54eb07625d75 Mon Sep 17 00:00:00 2001 From: Ashirg-ch Date: Thu, 23 Feb 2023 15:25:03 +0000 Subject: [PATCH 049/598] Update English (United States) translation --- locales/en-US.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/en-US.json b/locales/en-US.json index a5c16fd7..86b83a23 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -454,7 +454,7 @@ "footer_documentation": "Documentation", "footer_source_code": "Source code", "footer_original_source_code": "Original source code", - "footer_modfied_source_code": "Modified Source code", + "footer_modfied_source_code": "Modified source code", "adminprefs_modified_source_code_url_label": "URL to modified source code repository", "none": "none", "videoinfo_started_streaming_x_ago": "Started streaming `x` ago", From 23f1f8bde3ae838c26871eae16b1b3fbf37e11de Mon Sep 17 00:00:00 2001 From: Ashirg-ch Date: Thu, 23 Feb 2023 15:17:43 +0000 Subject: [PATCH 050/598] Update German translation --- locales/de.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/locales/de.json b/locales/de.json index 55c40905..c2941d6d 100644 --- a/locales/de.json +++ b/locales/de.json @@ -472,5 +472,12 @@ "search_filters_duration_option_none": "Beliebige Länge", "search_filters_date_label": "Upload-Datum", "search_filters_date_option_none": "Beliebiges Datum", - "error_video_not_in_playlist": "Das angeforderte Video existiert nicht in dieser Wiedergabeliste. Klicken Sie hier, um zur Startseite der Wiedergabeliste zu gelangen." + "error_video_not_in_playlist": "Das angeforderte Video existiert nicht in dieser Wiedergabeliste. Klicken Sie hier, um zur Startseite der Wiedergabeliste zu gelangen.", + "channel_tab_shorts_label": "Shorts", + "channel_tab_streams_label": "Livestreams", + "Music in this video": "Musik in diesem Video", + "Artist: ": "Künstler: ", + "Album: ": "Album: ", + "channel_tab_playlists_label": "Wiedergabelisten", + "channel_tab_channels_label": "Kanäle" } From eb3af9d4f101a5b99d26fe81b28d1789de3b4d7c Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Thu, 23 Feb 2023 17:29:11 +0000 Subject: [PATCH 051/598] Update Spanish translation --- locales/es.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/es.json b/locales/es.json index 6cf721f3..fec3a667 100644 --- a/locales/es.json +++ b/locales/es.json @@ -364,7 +364,7 @@ "footer_original_source_code": "Código fuente original", "adminprefs_modified_source_code_url_label": "URL al repositorio de código fuente modificado", "footer_source_code": "Código fuente", - "footer_modfied_source_code": "Código fuente modificado", + "footer_modfied_source_code": "Modificación del código fuente", "footer_donate_page": "Donar", "preferences_region_label": "País del contenido: ", "preferences_quality_dash_label": "Calidad de vídeo DASH preferida: ", From 0efb56238f9e75ca2d083cbd5c5701333b0bcd92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?O=C4=9Fuz=20Ersen?= Date: Sun, 26 Feb 2023 18:53:33 +0000 Subject: [PATCH 052/598] Update Turkish translation --- locales/tr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/tr.json b/locales/tr.json index d98e2038..b7cb3958 100644 --- a/locales/tr.json +++ b/locales/tr.json @@ -363,7 +363,7 @@ "footer_documentation": "Belgelendirme", "footer_source_code": "Kaynak Kodları", "footer_original_source_code": "Orijinal Kaynak Kodları", - "footer_modfied_source_code": "Değiştirilmiş Kaynak Kodları", + "footer_modfied_source_code": "Değiştirilmiş kaynak kodları", "adminprefs_modified_source_code_url_label": "Değiştirilmiş Kaynak Kodları Deposunun URL'si", "footer_donate_page": "Bağış Yap", "preferences_region_label": "İçerik Ülkesi: ", From 24ac873532bb562398d64afbd9e8f6cf943d283c Mon Sep 17 00:00:00 2001 From: maboroshin Date: Fri, 24 Feb 2023 05:24:09 +0000 Subject: [PATCH 053/598] Update Japanese translation --- locales/ja.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/locales/ja.json b/locales/ja.json index 3ad4b494..d08413ea 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -5,7 +5,7 @@ "generic_subscribers_count_0": "{{count}} 人の登録者", "generic_subscriptions_count_0": "{{count}} 個の登録チャンネル", "LIVE": "ライブ", - "Shared `x` ago": "`x`前に共有", + "Shared `x` ago": "`x`前に公開", "Unsubscribe": "登録解除", "Subscribe": "登録", "View channel on YouTube": "YouTube でチャンネルを見る", @@ -56,17 +56,17 @@ "preferences_category_player": "プレイヤーの設定", "preferences_video_loop_label": "常にループ: ", "preferences_autoplay_label": "自動再生: ", - "preferences_continue_label": "デフォルトで次を再生: ", + "preferences_continue_label": "次の動画を再生: ", "preferences_continue_autoplay_label": "次の動画を自動再生: ", "preferences_listen_label": "デフォルトで音声モードを使用: ", - "preferences_local_label": "動画をプロキシーに通す: ", - "preferences_speed_label": "デフォルトの再生速度: ", + "preferences_local_label": "動画視聴にプロキシーを経由: ", + "preferences_speed_label": "標準の再生速度: ", "preferences_quality_label": "優先する画質: ", "preferences_volume_label": "プレイヤーの音量: ", "preferences_comments_label": "デフォルトのコメント: ", "youtube": "YouTube", "reddit": "Reddit", - "preferences_captions_label": "デフォルトの字幕: ", + "preferences_captions_label": "優先する字幕: ", "Fallback captions: ": "フォールバック時の字幕: ", "preferences_related_videos_label": "関連動画を表示: ", "preferences_annotations_label": "デフォルトでアノテーションを表示: ", @@ -108,7 +108,7 @@ "Watch history": "再生履歴", "Delete account": "アカウントを削除", "preferences_category_admin": "管理者設定", - "preferences_default_home_label": "デフォルトのホーム: ", + "preferences_default_home_label": "ホームに表示するページ: ", "preferences_feed_menu_label": "フィードメニュー: ", "preferences_show_nick_label": "ニックネームを一番上に表示する: ", "Top enabled: ": "トップページを有効化: ", @@ -157,7 +157,7 @@ "Engagement: ": "エンゲージメント: ", "Whitelisted regions: ": "ホワイトリストの地域: ", "Blacklisted regions: ": "ブラックリストの地域: ", - "Shared `x`": "`x`に共有", + "Shared `x`": "公開日 `x`", "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 を無効にしているのかな?ここをクリックしてコメントを見れるけど、読み込みには少し時間がかかることがあるのを覚えておいてね。", @@ -191,9 +191,9 @@ "This channel does not exist.": "このチャンネルは存在しません。", "Could not get channel info.": "チャンネル情報を取得できませんでした。", "Could not fetch comments": "コメントを取得できませんでした", - "comments_view_x_replies_0": "{{count}} 件の返信を見る", + "comments_view_x_replies_0": "{{count}}件の返信を表示", "`x` ago": "`x`前", - "Load more": "もっと読み込む", + "Load more": "もっと見る", "comments_points_count_0": "{{count}}点", "Could not create mix.": "ミックスを作成できませんでした。", "Empty playlist": "空の再生リスト", @@ -377,8 +377,8 @@ "search_filters_duration_option_short": "4 分未満", "footer_documentation": "文書", "footer_source_code": "ソースコード", - "footer_original_source_code": "ソースコード (元)", - "footer_modfied_source_code": "ソースコード (改変)", + "footer_original_source_code": "元のソースコード", + "footer_modfied_source_code": "改変して使用", "adminprefs_modified_source_code_url_label": "改変されたソースコードのレポジトリのURL", "search_filters_duration_option_long": "20 分以上", "preferences_region_label": "地域: ", From fdf162e318ac3dd6c38e64a47938944efc730824 Mon Sep 17 00:00:00 2001 From: Milo Ivir Date: Sat, 25 Feb 2023 19:16:10 +0000 Subject: [PATCH 054/598] Update Croatian translation --- locales/hr.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/locales/hr.json b/locales/hr.json index 72cd6a8e..c626ed28 100644 --- a/locales/hr.json +++ b/locales/hr.json @@ -359,13 +359,13 @@ "next_steps_error_message_refresh": "Aktualiziraj stranicu", "next_steps_error_message_go_to_youtube": "Idi na YouTube", "footer_donate_page": "Doniraj", - "adminprefs_modified_source_code_url_label": "URL do repozitorija izmijenjenog izvornog koda", + "adminprefs_modified_source_code_url_label": "URL do repozitorija prilagođenog izvornog koda", "search_filters_duration_option_short": "Kratko (< 4 minute)", "search_filters_duration_option_long": "Dugo (> 20 minute)", "footer_source_code": "Izvorni kod", - "footer_modfied_source_code": "Izmijenjeni izvorni kod", + "footer_modfied_source_code": "Prilagođen izvorni kod", "footer_documentation": "Dokumentacija", - "footer_original_source_code": "Izvoran izvorni kod", + "footer_original_source_code": "Prvobitan izvorni kod", "preferences_region_label": "Zemlja sadržaja: ", "preferences_quality_dash_label": "Preferirana DASH videokvaliteta: ", "preferences_quality_option_dash": "DASH (adaptativna kvaliteta)", From 2974ed348cbb429ee780affa077c25d08d189995 Mon Sep 17 00:00:00 2001 From: Besnik Bleta Date: Thu, 23 Feb 2023 18:03:55 +0000 Subject: [PATCH 055/598] Update Albanian translation --- locales/sq.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/locales/sq.json b/locales/sq.json index 15025750..7f29a035 100644 --- a/locales/sq.json +++ b/locales/sq.json @@ -286,7 +286,7 @@ "search_filters_type_option_show": "Shfaqe", "search_filters_duration_option_short": "E shkurtër (< 4 minuta)", "search_filters_features_option_purchased": "Të blera", - "footer_modfied_source_code": "Kod Burim i ndryshuar", + "footer_modfied_source_code": "Kod burim i ndryshuar", "adminprefs_modified_source_code_url_label": "URL e depos së ndryshuar të kodit burim", "none": "asnjë", "videoinfo_started_streaming_x_ago": "Filloi transmetimin `x` më parë", @@ -468,5 +468,7 @@ "Artist: ": "Artist: ", "Album: ": "Album: ", "channel_tab_channels_label": "Kanale", - "Music in this video": "Muzikë në këtë video" + "Music in this video": "Muzikë në këtë video", + "channel_tab_shorts_label": "Të shkurtra", + "channel_tab_streams_label": "Transmetime të drejtpërdrejta" } From 27bf4d02a185e6750cdecdc4f1c169b0723dbbf5 Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Wed, 1 Mar 2023 22:08:19 -0500 Subject: [PATCH 056/598] PR nursing --- src/invidious/routes/api/v1/authenticated.cr | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/invidious/routes/api/v1/authenticated.cr b/src/invidious/routes/api/v1/authenticated.cr index dc86bb3c..a20d23d0 100644 --- a/src/invidious/routes/api/v1/authenticated.cr +++ b/src/invidious/routes/api/v1/authenticated.cr @@ -58,15 +58,16 @@ module Invidious::Routes::API::V1::Authenticated env.response.content_type = "application/json" user = env.get("user").as(User) - page = env.params.query["page"]?.try &.to_i? + page = env.params.query["page"]?.try &.to_i?.try &.clamp(0, Int32::MAX) page ||= 1 max_results = env.params.query["max_results"]?.try &.to_i?.try &.clamp(0, MAX_ITEMS_PER_PAGE) max_results ||= user.preferences.max_results max_results ||= CONFIG.default_user_preferences.max_results - if user.watched[(page - 1) * max_results]? - watched = user.watched.reverse[(page - 1) * max_results, max_results] + start_index = (page - 1) * max_results + if user.watched[start_index]? + watched = user.watched.reverse[start_index, max_results] end watched ||= [] of String From 4a1471346237f44481b4de823e87d393739e12c1 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Wed, 1 Mar 2023 23:39:07 -0500 Subject: [PATCH 057/598] use dig, create private image quality constant Co-Authored-By: Samantaz Fox --- src/invidious/channels/community.cr | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/invidious/channels/community.cr b/src/invidious/channels/community.cr index 87659c47..da8be6ea 100644 --- a/src/invidious/channels/community.cr +++ b/src/invidious/channels/community.cr @@ -1,3 +1,5 @@ +private IMAGE_QUALITIES = {320, 560, 640, 1280, 2000} + # TODO: Add "sort_by" def fetch_channel_community(ucid, continuation, locale, format, thin_mode) response = YT_POOL.client &.get("/channel/#{ucid}/community?gl=US&hl=en") @@ -75,10 +77,9 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode) json.field "author", author json.field "authorThumbnails" do json.array do - qualities = {32, 48, 76, 100, 176, 512} author_thumbnail = post["authorThumbnail"]["thumbnails"].as_a[0]["url"].as_s - qualities.each do |quality| + IMAGE_QUALITIES.each do |quality| json.object do json.field "url", author_thumbnail.gsub(/s\d+-/, "s#{quality}-") json.field "width", quality @@ -177,9 +178,7 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode) aspect_ratio = (width.to_f / height.to_f) url = thumbnail["url"].as_s.gsub(/=w\d+-h\d+(-p)?(-nd)?(-df)?(-rwa)?/, "=s640") - qualities = {320, 560, 640, 1280, 2000} - - qualities.each do |quality| + IMAGE_QUALITIES.each do |quality| json.object do json.field "url", url.gsub(/=s\d+/, "=s#{quality}") json.field "width", quality @@ -196,7 +195,7 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode) json.array do attachment["choices"].as_a.each do |choice| json.object do - json.field "text", choice["text"]["runs"][0]["text"].as_s + json.field "text", choice.dig("text", "runs", 0, "text").as_s # A choice can have an image associated with it. # Ex post: https://www.youtube.com/post/UgkxD4XavXUD4NQiddJXXdohbwOwcVqrH9Re if choice["image"]? @@ -205,10 +204,9 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode) height = thumbnail["height"].as_i aspect_ratio = (width.to_f / height.to_f) url = thumbnail["url"].as_s.gsub(/=w\d+-h\d+(-p)?(-nd)?(-df)?(-rwa)?/, "=s640") - qualities = {320, 560, 640, 1280, 2000} json.field "image" do json.array do - qualities.each do |quality| + IMAGE_QUALITIES.each do |quality| json.object do json.field "url", url.gsub(/=s\d+/, "=s#{quality}") json.field "width", quality @@ -235,9 +233,7 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode) aspect_ratio = (width.to_f / height.to_f) url = thumbnail["url"].as_s.gsub(/=w\d+-h\d+(-p)?(-nd)?(-df)?(-rwa)?/, "=s640") - qualities = {320, 560, 640, 1280, 2000} - - qualities.each do |quality| + IMAGE_QUALITIES.each do |quality| json.object do json.field "url", url.gsub(/=s\d+/, "=s#{quality}") json.field "width", quality From 406d74d0b6c85f300b7a96f85acb0963c998f944 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Wed, 1 Mar 2023 21:09:00 +0000 Subject: [PATCH 058/598] Update Spanish translation --- locales/es.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/es.json b/locales/es.json index fec3a667..6cf721f3 100644 --- a/locales/es.json +++ b/locales/es.json @@ -364,7 +364,7 @@ "footer_original_source_code": "Código fuente original", "adminprefs_modified_source_code_url_label": "URL al repositorio de código fuente modificado", "footer_source_code": "Código fuente", - "footer_modfied_source_code": "Modificación del código fuente", + "footer_modfied_source_code": "Código fuente modificado", "footer_donate_page": "Donar", "preferences_region_label": "País del contenido: ", "preferences_quality_dash_label": "Calidad de vídeo DASH preferida: ", From 60b7c8015c9ae77664d0b0680a81cfcc979d5a03 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Thu, 2 Mar 2023 07:29:44 -0500 Subject: [PATCH 059/598] add channel emoji css class --- assets/css/default.css | 4 ++++ src/invidious/comments.cr | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/assets/css/default.css b/assets/css/default.css index 9788e9f7..5ec79a43 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -565,3 +565,7 @@ p, /* Wider settings name to less word wrap */ .pure-form-aligned .pure-control-group label { width: 19em; } + +.channel-emoji { + margin: 0 2px; +} diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr index 6c323bc1..56622dec 100644 --- a/src/invidious/comments.cr +++ b/src/invidious/comments.cr @@ -701,7 +701,7 @@ def content_to_comment_html(content, video_id : String? = "") str << %(title=") << emojiAlt << "\" " str << %(width=") << emojiThumb["width"] << "\" " str << %(height=") << emojiThumb["height"] << "\" " - str << %(style="margin-right:2px;margin-left:2px;"/>) + str << %(class="channel-emoji"/>) end else # Hide deleted channel emoji From 8c0efb3ea9e409796ae860128b16d8aac860c5c6 Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Thu, 2 Mar 2023 14:45:26 -0500 Subject: [PATCH 060/598] validate video id --- src/invidious/routes/api/v1/authenticated.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/routes/api/v1/authenticated.cr b/src/invidious/routes/api/v1/authenticated.cr index a20d23d0..75dad6df 100644 --- a/src/invidious/routes/api/v1/authenticated.cr +++ b/src/invidious/routes/api/v1/authenticated.cr @@ -94,7 +94,7 @@ module Invidious::Routes::API::V1::Authenticated user = env.get("user").as(User) id = env.params.url["id"]?.try &.as(String) - if !id + if !id.match(/[a-zA-Z0-9_-]{11}/) return error_json(400, "Invalid video id.") end From 38f6d08be6559915262cd246b7a82988700250a5 Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Thu, 2 Mar 2023 14:47:14 -0500 Subject: [PATCH 061/598] Validate id, avoid db call if not needed --- src/invidious/routes/api/v1/authenticated.cr | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/invidious/routes/api/v1/authenticated.cr b/src/invidious/routes/api/v1/authenticated.cr index 75dad6df..e8e7c524 100644 --- a/src/invidious/routes/api/v1/authenticated.cr +++ b/src/invidious/routes/api/v1/authenticated.cr @@ -82,7 +82,7 @@ module Invidious::Routes::API::V1::Authenticated end id = env.params.url["id"]?.try &.as(String) - if !id + if !id.match(/[a-zA-Z0-9_-]{11}/) return error_json(400, "Invalid video id.") end @@ -93,6 +93,10 @@ module Invidious::Routes::API::V1::Authenticated def self.mark_unwatched(env) user = env.get("user").as(User) + if !user.preferences.watch_history + return error_json(409, "Watch history is disabled in preferences.") + end + id = env.params.url["id"]?.try &.as(String) if !id.match(/[a-zA-Z0-9_-]{11}/) return error_json(400, "Invalid video id.") From a5cc66e060578f801371fe3f4b53bcb3d61b3ef9 Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Thu, 2 Mar 2023 16:11:50 -0500 Subject: [PATCH 062/598] Fix id check --- src/invidious/routes/api/v1/authenticated.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/invidious/routes/api/v1/authenticated.cr b/src/invidious/routes/api/v1/authenticated.cr index e8e7c524..a024736c 100644 --- a/src/invidious/routes/api/v1/authenticated.cr +++ b/src/invidious/routes/api/v1/authenticated.cr @@ -81,7 +81,7 @@ module Invidious::Routes::API::V1::Authenticated return error_json(409, "Watch history is disabled in preferences.") end - id = env.params.url["id"]?.try &.as(String) + id = env.params.url["id"] if !id.match(/[a-zA-Z0-9_-]{11}/) return error_json(400, "Invalid video id.") end @@ -97,7 +97,7 @@ module Invidious::Routes::API::V1::Authenticated return error_json(409, "Watch history is disabled in preferences.") end - id = env.params.url["id"]?.try &.as(String) + id = env.params.url["id"] if !id.match(/[a-zA-Z0-9_-]{11}/) return error_json(400, "Invalid video id.") end From 03542f2f5dcf7686b8d5fd38bb0c8c0e9e4a2cb7 Mon Sep 17 00:00:00 2001 From: amogusussy <83502633+amogusussy@users.noreply.github.com> Date: Fri, 3 Mar 2023 22:28:26 +0000 Subject: [PATCH 063/598] Fix empty description boxes. If a video has no description, (without this commit) the description box will still take up 8.3em, even if there's no content in it. This fixes that issue. --- assets/css/default.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/css/default.css b/assets/css/default.css index 3deaebe1..24910610 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -515,7 +515,7 @@ hr { #descexpansionbutton ~ div { overflow: hidden; - height: 8.3em; + max-height: 8.3em; } #descexpansionbutton:checked ~ div { From a3ecd46b019637b9a9d926d91042bbf3603c7f7c Mon Sep 17 00:00:00 2001 From: Paul Fauchon Date: Sun, 5 Mar 2023 04:55:27 +0800 Subject: [PATCH 064/598] add new Android client to list of projects using invidious --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 0744ac50..abf57e38 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,7 @@ Weblate also allows you to log-in with major SSO providers like Github, Gitlab, - [TubiTui](https://codeberg.org/777/TubiTui): A lightweight, libre, TUI-based YouTube client. - [Ytfzf](https://github.com/pystardust/ytfzf): A posix script to find and watch youtube videos from the terminal. (Without API) - [Playlet](https://github.com/iBicha/playlet): Unofficial Youtube client for Roku TV +- [Clipious](https://github.com/lamarios/clipious): Unofficial Invidious client for Android ## Liability From 025e7555420a88757aa8709419e8f09ba654854d Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Sat, 4 Mar 2023 19:14:28 -0500 Subject: [PATCH 065/598] Use single db call --- src/invidious/database/users.cr | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/invidious/database/users.cr b/src/invidious/database/users.cr index f8422874..d54e6a76 100644 --- a/src/invidious/database/users.cr +++ b/src/invidious/database/users.cr @@ -50,10 +50,9 @@ module Invidious::Database::Users end def mark_watched(user : User, vid : String) - mark_unwatched(user, vid) request = <<-SQL UPDATE users - SET watched = array_append(watched, $1) + SET watched = array_append(array_remove(watched, $1), $1) WHERE email = $2 SQL From 3c3d9ebf84f6dbac671dd7561b72de4af45f1747 Mon Sep 17 00:00:00 2001 From: fresh Date: Sat, 4 Mar 2023 23:07:28 +0000 Subject: [PATCH 066/598] Update Greek translation --- locales/el.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/el.json b/locales/el.json index 3448a4dc..8d0c84dd 100644 --- a/locales/el.json +++ b/locales/el.json @@ -366,7 +366,7 @@ "preferences_quality_option_hd720": "HD720", "preferences_quality_option_medium": "Μεσαία", "preferences_quality_option_small": "Μικρό", - "preferences_quality_option_dash": "DASH (προσαρμοστική ποιότητα)", + "preferences_quality_option_dash": "DASH (προσαρμόσιμη ποιότητα)", "preferences_quality_dash_option_4320p": "4320p", "preferences_quality_dash_option_720p": "720p", "invidious": "Invidious", @@ -450,5 +450,5 @@ "search_filters_type_option_show": "Μπάρα προόδου διαβάσματος", "preferences_watch_history_label": "Ενεργοποίηση ιστορικού παρακολούθησης: ", "search_filters_title": "Φίλτρο", - "search_message_no_results": "Δεν" + "search_message_no_results": "Δε βρέθηκαν αποτελέσματα." } From 1f607273a87c85e65c7bfc7345984c6b7d631a5a Mon Sep 17 00:00:00 2001 From: Felipe Nogueira Date: Sun, 5 Mar 2023 02:24:43 +0000 Subject: [PATCH 067/598] Update Portuguese (Brazil) translation --- locales/pt-BR.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/pt-BR.json b/locales/pt-BR.json index 079c4ea1..ec00d46e 100644 --- a/locales/pt-BR.json +++ b/locales/pt-BR.json @@ -381,7 +381,7 @@ "footer_documentation": "Documentação", "footer_source_code": "Código fonte", "footer_original_source_code": "Código fonte original", - "footer_modfied_source_code": "Código Fonte Modificado", + "footer_modfied_source_code": "Código-fonte modificado", "preferences_quality_dash_label": "Qualidade de vídeo do painel preferida: ", "preferences_region_label": "País do conteúdo: ", "preferences_quality_dash_option_4320p": "4320p", From 9325fa79ae3ee751b52da95997cc4824742531e7 Mon Sep 17 00:00:00 2001 From: VisualPlugin Date: Mon, 6 Mar 2023 06:17:50 +0000 Subject: [PATCH 068/598] Update es.json --- locales/es.json | 106 ++++++++++++++++++++++++------------------------ 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/locales/es.json b/locales/es.json index 6cf721f3..a0d16325 100644 --- a/locales/es.json +++ b/locales/es.json @@ -52,21 +52,21 @@ "preferences_video_loop_label": "Repetir siempre: ", "preferences_autoplay_label": "Reproducción automática: ", "preferences_continue_label": "Reproducir siguiente por defecto: ", - "preferences_continue_autoplay_label": "Reproducir automáticamente el vídeo siguiente: ", + "preferences_continue_autoplay_label": "Reproducir automáticamente el video siguiente: ", "preferences_listen_label": "Activar el sonido por defecto: ", - "preferences_local_label": "¿Usar un proxy para los vídeos? ", + "preferences_local_label": "¿Usar un proxy para los videos? ", "preferences_speed_label": "Velocidad por defecto: ", - "preferences_quality_label": "Calidad de vídeo preferida: ", + "preferences_quality_label": "Calidad de video preferida: ", "preferences_volume_label": "Volumen del reproductor: ", "preferences_comments_label": "Comentarios por defecto: ", "youtube": "YouTube", "reddit": "Reddit", "preferences_captions_label": "Subtítulos por defecto: ", "Fallback captions: ": "Subtítulos alternativos: ", - "preferences_related_videos_label": "¿Mostrar vídeos relacionados? ", + "preferences_related_videos_label": "¿Mostrar videos relacionados? ", "preferences_annotations_label": "¿Mostrar anotaciones por defecto? ", - "preferences_extend_desc_label": "Extender automáticamente la descripción del vídeo: ", - "preferences_vr_mode_label": "Vídeos interactivos de 360 grados (necesita WebGL): ", + "preferences_extend_desc_label": "Extender automáticamente la descripción del video: ", + "preferences_vr_mode_label": "Videos interactivos de 360 grados (necesita WebGL): ", "preferences_category_visual": "Preferencias visuales", "preferences_player_style_label": "Estilo de reproductor: ", "Dark mode: ": "Modo oscuro: ", @@ -79,16 +79,16 @@ "preferences_category_subscription": "Preferencias de la suscripción", "preferences_annotations_subscribed_label": "¿Mostrar anotaciones por defecto para los canales suscritos? ", "Redirect homepage to feed: ": "Redirigir la página de inicio a la fuente: ", - "preferences_max_results_label": "Número de vídeos mostrados en la fuente: ", - "preferences_sort_label": "Ordenar los vídeos por: ", + "preferences_max_results_label": "Número de videos mostrados en la fuente: ", + "preferences_sort_label": "Ordenar los videos por: ", "published": "fecha de publicación", "published - reverse": "fecha de publicación: orden inverso", "alphabetically": "alfabéticamente", "alphabetically - reverse": "alfabéticamente: orden inverso", "channel name": "nombre del canal", "channel name - reverse": "nombre del canal: orden inverso", - "Only show latest video from channel: ": "Mostrar solo el último vídeo del canal: ", - "Only show latest unwatched video from channel: ": "Mostrar solo el último vídeo sin ver del canal: ", + "Only show latest video from channel: ": "Mostrar solo el último video del canal: ", + "Only show latest unwatched video from channel: ": "Mostrar solo el último video sin ver del canal: ", "preferences_unseen_only_label": "Mostrar solo los no vistos: ", "preferences_notifications_only_label": "Mostrar solo notificaciones (si hay alguna): ", "Enable web notifications": "Habilitar notificaciones web", @@ -139,7 +139,7 @@ "Editing playlist `x`": "Editando la lista de reproducción 'x'", "Show more": "Mostrar más", "Show less": "Mostrar menos", - "Watch on YouTube": "Ver el vídeo en YouTube", + "Watch on YouTube": "Ver en YouTube", "Switch Invidious Instance": "Cambiar Instancia de Invidious", "Hide annotations": "Ocultar anotaciones", "Show annotations": "Mostrar anotaciones", @@ -153,7 +153,7 @@ "Shared `x`": "Compartido `x`", "Premieres in `x`": "Se estrena en `x`", "Premieres `x`": "Estrenos `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.": "¡Hola! Parece que tiene JavaScript desactivado. Haga clic aquí para ver los comentarios, pero tenga en cuenta que pueden tardar un poco más en cargarse.", + "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "¡Hola! Parece que tienes JavaScript desactivado. Haz clic aquí para ver los comentarios, pero tengas en cuenta que pueden tardar un poco más en cargarse.", "View YouTube comments": "Ver los comentarios de YouTube", "View more comments on Reddit": "Ver más comentarios en Reddit", "View `x` comments": { @@ -164,7 +164,7 @@ "Hide replies": "Ocultar las respuestas", "Show replies": "Mostrar las respuestas", "Incorrect password": "Contraseña incorrecta", - "Quota exceeded, try again in a few hours": "Cuota excedida, pruebe otra vez en unas horas", + "Quota exceeded, try again in a few hours": "Cuota excedida, prueba otra vez en unas horas", "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "No se puede iniciar sesión, asegúrese de que la autentificación de dos factores (autentificador o SMS) esté habilitada.", "Invalid TFA code": "Código TFA no válido", "Login failed. This may be because two-factor authentication is not turned on for your account.": "Error de inicio de sesion. Puede deberse a que la autentificación de dos factores no está habilitada en su cuenta.", @@ -176,7 +176,7 @@ "Wrong username or password": "Nombre o contraseña incorrecto", "Please sign in using 'Log in with Google'": "Inicie sesión con «Iniciar sesión con Google»", "Password cannot be empty": "La contraseña no puede estar en blanco", - "Password cannot be longer than 55 characters": "La contraseña no puede tener más de 55 caracteres", + "Password cannot be longer than 55 characters": "La contraseña no debe tener más de 55 caracteres", "Please log in": "Inicie sesión, por favor", "Invidious Private Feed for `x`": "Fuente privada de Invidious para `x`", "channel:`x`": "canal: `x`", @@ -198,7 +198,7 @@ "No such user": "Usuario no existe", "Token is expired, please try again": "El símbolo ha caducado, inténtelo de nuevo", "English": "Inglés", - "English (auto-generated)": "Inglés (generados automáticamente)", + "English (auto-generated)": "Inglés (generado automáticamente)", "Afrikaans": "Afrikáans", "Albanian": "Albanés", "Amharic": "Amárico", @@ -324,50 +324,51 @@ "permalink": "enlace permanente", "`x` marked it with a ❤": "`x` lo ha marcado con un ❤", "Audio mode": "Modo de audio", - "Video mode": "Modo de vídeo", - "channel_tab_videos_label": "Vídeos", + "Video mode": "Modo de video", + "channel_tab_videos_label": "Videos", "Playlists": "Listas de reproducción", "channel_tab_community_label": "Comunidad", - "search_filters_sort_option_relevance": "relevancia", - "search_filters_sort_option_rating": "valoración", - "search_filters_sort_option_date": "fecha", - "search_filters_sort_option_views": "visualizaciones", - "search_filters_type_label": "content_type", + "search_filters_sort_option_relevance": "Relevancia", + "search_filters_sort_option_rating": "Valoración", + "search_filters_sort_option_date": "Fecha de subida", + "search_filters_sort_option_views": "Visualizaciones", + "search_filters_type_label": "tipo de contenido", "search_filters_duration_label": "duración", "search_filters_features_label": "funcionalidades", "search_filters_sort_label": "ordenar", - "search_filters_date_option_hour": "hora", - "search_filters_date_option_today": "hoy", - "search_filters_date_option_week": "semana", - "search_filters_date_option_month": "mes", - "search_filters_date_option_year": "año", - "search_filters_type_option_video": "vídeo", - "search_filters_type_option_channel": "canal", - "search_filters_type_option_playlist": "lista de reproducción", - "search_filters_type_option_movie": "película", - "search_filters_type_option_show": "programa", - "search_filters_features_option_hd": "hd", - "search_filters_features_option_subtitles": "subtítulos", - "search_filters_features_option_c_commons": "creative_commons", - "search_filters_features_option_three_d": "3d", - "search_filters_features_option_live": "directo", - "search_filters_features_option_four_k": "4k", - "search_filters_features_option_location": "ubicación", - "search_filters_features_option_hdr": "hdr", + "search_filters_date_option_hour": "Última hora", + "search_filters_date_option_today": "Hoy", + "search_filters_date_option_week": "Esta semana", + "search_filters_date_option_month": "Este mes", + "search_filters_date_option_year": "Este año", + "search_filters_type_option_video": "Video", + "search_filters_type_option_channel": "Canal", + "search_filters_type_option_playlist": "Lista de reproducción", + "search_filters_type_option_movie": "Película", + "search_filters_type_option_show": "Programa", + "search_filters_features_option_hd": "HD", + "search_filters_features_option_subtitles": "Subtítulos", + "search_filters_features_option_c_commons": "Creative Commons", + "search_filters_features_option_three_d": "3D", + "search_filters_features_option_live": "En directo", + "search_filters_features_option_four_k": "4K", + "search_filters_features_option_location": "Ubicación", + "search_filters_features_option_hdr": "HDR", "Current version: ": "Versión actual: ", "next_steps_error_message": "Después de lo cual debes intentar: ", "next_steps_error_message_refresh": "Recargar la página", "next_steps_error_message_go_to_youtube": "Ir a YouTube", - "search_filters_duration_option_short": "Corto (< 4 minutos)", - "search_filters_duration_option_long": "Largo (> 20 minutos)", + "search_filters_duration_option_short": "Menos de 4 minutos", + "search_filters_duration_option_medium": "De 4 a 20 minutos", + "search_filters_duration_option_long": "Más de 20 minutos", "footer_documentation": "Documentación", "footer_original_source_code": "Código fuente original", - "adminprefs_modified_source_code_url_label": "URL al repositorio de código fuente modificado", + "adminprefs_modified_source_code_url_label": "Enlace al repositorio de código fuente modificado", "footer_source_code": "Código fuente", "footer_modfied_source_code": "Código fuente modificado", "footer_donate_page": "Donar", "preferences_region_label": "País del contenido: ", - "preferences_quality_dash_label": "Calidad de vídeo DASH preferida: ", + "preferences_quality_dash_label": "Calidad de video DASH preferida: ", "preferences_quality_option_hd720": "HD720", "preferences_quality_option_medium": "Intermedia", "preferences_quality_dash_option_auto": "Automática", @@ -376,7 +377,7 @@ "download_subtitles": "Subtítulos- `x` (.vtt)", "user_created_playlists": "`x` listas de reproducción creadas", "user_saved_playlists": "`x` listas de reproducción guardadas", - "Video unavailable": "Vídeo no disponible", + "Video unavailable": "Video no disponible", "videoinfo_youTube_embed_link": "Insertar", "preferences_quality_dash_option_2160p": "2160p", "preferences_quality_dash_option_4320p": "4320p", @@ -413,8 +414,8 @@ "generic_count_weeks_plural": "{{count}} semanas", "generic_playlists_count": "{{count}} lista de reproducción", "generic_playlists_count_plural": "{{count}} listas de reproducción", - "generic_videos_count": "{{count}} vídeo", - "generic_videos_count_plural": "{{count}} vídeos", + "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", @@ -433,7 +434,7 @@ "crash_page_search_issue": "buscado problemas existentes en GitHub", "crash_page_you_found_a_bug": "¡Parece que has encontrado un error en Invidious!", "crash_page_refresh": "probado a recargar la página", - "crash_page_report_issue": "Si nada de lo anterior ha sido de ayuda, por favor, abre una nueva incidencia en GitHub (preferiblemente en inglés) e incluye el siguiente texto en tu mensaje (NO traduzcas este texto):", + "crash_page_report_issue": "Si nada de lo anterior ha sido de ayuda, por favor, abre una nueva incidencia en GitHub (preferiblemente en inglés) e incluye verbatim el siguiente texto en tu mensaje:", "English (United States)": "Inglés (Estados Unidos)", "Cantonese (Hong Kong)": "Cantonés (Hong Kong)", "Dutch (auto-generated)": "Neerlandés (generados automáticamente)", @@ -461,23 +462,22 @@ "search_message_no_results": "No se han encontrado resultados.", "search_message_change_filters_or_query": "Pruebe ampliar la consulta de búsqueda y/o a cambiar los filtros.", "search_filters_title": "Filtros", - "search_filters_date_label": "Fecha de subida", + "search_filters_date_label": "fecha de subida", "search_filters_date_option_none": "Cualquier fecha", "search_filters_type_option_all": "Cualquier tipo", "search_filters_duration_option_none": "Cualquier duración", "search_filters_features_option_vr180": "VR180", - "search_filters_apply_button": "Aplicar filtros seleccionados", + "search_filters_apply_button": "Aplicar filtros", "tokens_count": "{{count}} ficha", "tokens_count_plural": "{{count}} fichas", "search_message_use_another_instance": " También puede buscar en otra instancia.", - "search_filters_duration_option_medium": "Medio (4 - 20 minutes)", "Popular enabled: ": "¿Habilitar la sección popular? ", - "error_video_not_in_playlist": "El vídeo solicitado no existe en esta lista de reproducción. Haga clic aquí para acceder a la página de inicio de la lista de reproducción.", + "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.", "channel_tab_streams_label": "Directos", "channel_tab_channels_label": "Canales", "channel_tab_shorts_label": "Cortos", "channel_tab_playlists_label": "Listas de reproducción", - "Music in this video": "Música en este vídeo", + "Music in this video": "Música en este video", "Artist: ": "Artista: ", "Album: ": "Álbum: " } From 548a0f26ef07a4f2beec3f5e7b7d2b667b9ff50e Mon Sep 17 00:00:00 2001 From: maboroshin Date: Sun, 5 Mar 2023 23:53:05 +0000 Subject: [PATCH 069/598] Update Japanese translation --- locales/ja.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/ja.json b/locales/ja.json index d08413ea..4d2ed5a0 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -366,7 +366,7 @@ "search_filters_features_option_subtitles": "字幕", "search_filters_features_option_c_commons": "クリエイティブ・コモンズ", "search_filters_features_option_three_d": "3D", - "search_filters_features_option_live": "生配信", + "search_filters_features_option_live": "ライブ", "search_filters_features_option_four_k": "4K", "search_filters_features_option_location": "場所", "search_filters_features_option_hdr": "HDR", From d8e23d34b63b8f4f34da5c6b4bddf6eb46a3a828 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 7 Mar 2023 11:38:09 -0500 Subject: [PATCH 070/598] add song title for music tracks --- locales/en-US.json | 1 + src/invidious/jsonify/api_v1/video_json.cr | 15 +++++++++++++++ src/invidious/videos.cr | 2 +- src/invidious/videos/music.cr | 3 ++- src/invidious/videos/parser.cr | 5 ++++- src/invidious/views/watch.ecr | 7 ++++--- 6 files changed, 27 insertions(+), 6 deletions(-) diff --git a/locales/en-US.json b/locales/en-US.json index 86b83a23..65a81ab7 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -190,6 +190,7 @@ "Blacklisted regions: ": "Blacklisted regions: ", "Music in this video": "Music in this video", "Artist: ": "Artist: ", + "Song: ": "Song: ", "Album: ": "Album: ", "Shared `x`": "Shared `x`", "Premieres in `x`": "Premieres in `x`", diff --git a/src/invidious/jsonify/api_v1/video_json.cr b/src/invidious/jsonify/api_v1/video_json.cr index a2b1a35c..fe4b5223 100644 --- a/src/invidious/jsonify/api_v1/video_json.cr +++ b/src/invidious/jsonify/api_v1/video_json.cr @@ -197,6 +197,21 @@ module Invidious::JSONify::APIv1 end end + if !video.music.empty? + json.field "musicTracks" do + json.array do + video.music.each do |music| + json.object do + json.field "song", music.song + json.field "artist", music.artist + json.field "album", music.album + json.field "license", music.license + end + end + end + end + end + json.field "recommendedVideos" do json.array do video.related_videos.each do |rv| diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index 436ac82d..86f5ada4 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -249,7 +249,7 @@ struct Video def music : Array(VideoMusic) info["music"].as_a.map { |music_json| - VideoMusic.new(music_json["album"].as_s, music_json["artist"].as_s, music_json["license"].as_s) + VideoMusic.new(music_json["song"].as_s, music_json["album"].as_s, music_json["artist"].as_s, music_json["license"].as_s) } end diff --git a/src/invidious/videos/music.cr b/src/invidious/videos/music.cr index 402ae46f..08d88a3e 100644 --- a/src/invidious/videos/music.cr +++ b/src/invidious/videos/music.cr @@ -3,10 +3,11 @@ require "json" struct VideoMusic include JSON::Serializable + property song : String property album : String property artist : String property license : String - def initialize(@album : String, @artist : String, @license : String) + def initialize(@song : String, @album : String, @artist : String, @license : String) end end diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr index cf43f1be..1a8c25e4 100644 --- a/src/invidious/videos/parser.cr +++ b/src/invidious/videos/parser.cr @@ -322,6 +322,7 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any music_desclist.try &.as_a.each do |music_desc| artist = nil + song = nil album = nil music_license = nil @@ -329,13 +330,15 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any desc_title = extract_text(desc.dig?("infoRowRenderer", "title")) if desc_title == "ARTIST" artist = extract_text(desc.dig?("infoRowRenderer", "defaultMetadata")) + elsif desc_title == "SONG" + song = extract_text(desc.dig?("infoRowRenderer", "defaultMetadata")) elsif desc_title == "ALBUM" album = extract_text(desc.dig?("infoRowRenderer", "defaultMetadata")) elsif desc_title == "LICENSES" music_license = extract_text(desc.dig?("infoRowRenderer", "expandedMetadata")) end end - music_list << VideoMusic.new(album.to_s, artist.to_s, music_license.to_s) + music_list << VideoMusic.new(song.to_s, album.to_s, artist.to_s, music_license.to_s) end # Author infos diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 666eb3b0..01b30f7a 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -248,9 +248,10 @@ we're going to need to do it here in order to allow for translations.
<% video.music.each do |music| %>
-

<%= translate(locale, "Artist: ") %><%= music.artist %>

-

<%= translate(locale, "Album: ") %><%= music.album %>

-

<%= translate(locale, "License: ") %><%= music.license %>

+

<%= translate(locale, "Song: ") %><%= music.song %>

+

<%= translate(locale, "Artist: ") %><%= music.artist %>

+

<%= translate(locale, "Album: ") %><%= music.album %>

+

<%= translate(locale, "License: ") %><%= music.license %>

<% end %>
From 742c951bc9fdc6eb1e5687104e67500fb778e0ea Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 7 Mar 2023 13:06:15 -0500 Subject: [PATCH 071/598] support videos with multiple songs --- src/invidious/videos/parser.cr | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr index 1a8c25e4..722c90e8 100644 --- a/src/invidious/videos/parser.cr +++ b/src/invidious/videos/parser.cr @@ -322,10 +322,17 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any music_desclist.try &.as_a.each do |music_desc| artist = nil - song = nil album = nil music_license = nil + # used when multiple songs + song = music_desc.dig?("carouselLockupRenderer", "videoLockup", "compactVideoRenderer", "title", "simpleText") + + # used when multiple songs and the song has a link + if !song + song = music_desc.dig("carouselLockupRenderer", "videoLockup", "compactVideoRenderer", "title", "runs", 0, "text") + end + music_desc.dig?("carouselLockupRenderer", "infoRows").try &.as_a.each do |desc| desc_title = extract_text(desc.dig?("infoRowRenderer", "title")) if desc_title == "ARTIST" From 0b17f68ebacdb54e74116cf3364c8229e896eff0 Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Tue, 7 Mar 2023 13:50:02 -0500 Subject: [PATCH 072/598] Fix input validation --- src/invidious/routes/api/v1/authenticated.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/invidious/routes/api/v1/authenticated.cr b/src/invidious/routes/api/v1/authenticated.cr index a024736c..ce2ee812 100644 --- a/src/invidious/routes/api/v1/authenticated.cr +++ b/src/invidious/routes/api/v1/authenticated.cr @@ -82,7 +82,7 @@ module Invidious::Routes::API::V1::Authenticated end id = env.params.url["id"] - if !id.match(/[a-zA-Z0-9_-]{11}/) + if !id.match(/^[a-zA-Z0-9_-]{11}$/) return error_json(400, "Invalid video id.") end @@ -98,7 +98,7 @@ module Invidious::Routes::API::V1::Authenticated end id = env.params.url["id"] - if !id.match(/[a-zA-Z0-9_-]{11}/) + if !id.match(/^[a-zA-Z0-9_-]{11}$/) return error_json(400, "Invalid video id.") end From e3081ef1a93973fe10ba8508ad31d257d641350e Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 7 Mar 2023 14:23:08 -0500 Subject: [PATCH 073/598] Apply style change suggestions Co-authored-by: Samantaz Fox --- src/invidious/videos.cr | 7 ++++++- src/invidious/videos/parser.cr | 10 ++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index 86f5ada4..0038a97a 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -249,7 +249,12 @@ struct Video def music : Array(VideoMusic) info["music"].as_a.map { |music_json| - VideoMusic.new(music_json["song"].as_s, music_json["album"].as_s, music_json["artist"].as_s, music_json["license"].as_s) + VideoMusic.new( + music_json["song"].as_s, + music_json["album"].as_s, + music_json["artist"].as_s, + music_json["license"].as_s + ) } end diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr index 722c90e8..7cfc7ea7 100644 --- a/src/invidious/videos/parser.cr +++ b/src/invidious/videos/parser.cr @@ -325,12 +325,10 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any album = nil music_license = nil - # used when multiple songs - song = music_desc.dig?("carouselLockupRenderer", "videoLockup", "compactVideoRenderer", "title", "simpleText") - - # used when multiple songs and the song has a link - if !song - song = music_desc.dig("carouselLockupRenderer", "videoLockup", "compactVideoRenderer", "title", "runs", 0, "text") + # Used when the video has multiple songs + if song_title = music_desc.dig?("carouselLockupRenderer", "videoLockup", "compactVideoRenderer", "title") + # "simpleText" for plain text / "runs" when song has a link + song = song_title["simpleText"]? || song_title.dig("runs", 0, "text") end music_desc.dig?("carouselLockupRenderer", "infoRows").try &.as_a.each do |desc| From a781cf37347e97c469eb098e95c9a80482aac1b9 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 7 Mar 2023 15:59:51 -0500 Subject: [PATCH 074/598] readd try as bool for isSponsor key --- src/invidious/comments.cr | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr index 56622dec..b15d63d4 100644 --- a/src/invidious/comments.cr +++ b/src/invidious/comments.cr @@ -334,7 +334,8 @@ def template_youtube_comments(comments, locale, thin_mode, is_replies = false) elsif child["verified"]?.try &.as_bool author_name += " " end - if child["isSponsor"].as_bool + + if child["isSponsor"]?.try &.as_bool sponsor_icon = String.build do |str| str << %( Date: Tue, 7 Mar 2023 15:46:36 -0800 Subject: [PATCH 075/598] 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 3848c3f53f230e971eb67b1317f2cd4ad1b76176 Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Sun, 12 Mar 2023 18:36:03 -0400 Subject: [PATCH 076/598] Update src/invidious/routes/video_playback.cr Co-authored-by: Samantaz Fox --- src/invidious/routes/video_playback.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/routes/video_playback.cr b/src/invidious/routes/video_playback.cr index f24c0ded..9641e01a 100644 --- a/src/invidious/routes/video_playback.cr +++ b/src/invidious/routes/video_playback.cr @@ -278,7 +278,7 @@ module Invidious::Routes::VideoPlayback end if itag.nil? - fmt = video.fmt_stream[-1] + fmt = video.fmt_stream[-1]? else fmt = video.fmt_stream.find(nil) { |f| f["itag"].as_i == itag } || video.adaptive_fmts.find(nil) { |f| f["itag"].as_i == itag } end From ffcc837c2adcb4faac104c08c32060a475730e2b Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Sun, 12 Mar 2023 18:50:01 -0400 Subject: [PATCH 077/598] remove music license --- src/invidious/views/watch.ecr | 1 - 1 file changed, 1 deletion(-) diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 01b30f7a..ce92a546 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -251,7 +251,6 @@ we're going to need to do it here in order to allow for translations.

<%= translate(locale, "Song: ") %><%= music.song %>

<%= translate(locale, "Artist: ") %><%= music.artist %>

<%= translate(locale, "Album: ") %><%= music.album %>

-

<%= translate(locale, "License: ") %><%= music.license %>

<% end %>
From 712aea0831cff6a071b303ac3cc25a3559147917 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane?= Date: Wed, 15 Mar 2023 19:11:17 +0100 Subject: [PATCH 078/598] chore: update HoloPlay app on README (#3690) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index abf57e38..602ad2e2 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,7 @@ Weblate also allows you to log-in with major SSO providers like Github, Gitlab, - [CloudTube](https://sr.ht/~cadence/tube/): A JavaScript-rich alternate YouTube player. - [PeerTubeify](https://gitlab.com/Cha_de_L/peertubeify): On YouTube, displays a link to the same video on PeerTube, if it exists. - [MusicPiped](https://github.com/deep-gaurav/MusicPiped): A material design music player that streams music from YouTube. -- [HoloPlay](https://github.com/stephane-r/HoloPlay): Funny Android application connecting on Invidious API's with search, playlists and favorites. +- [HoloPlay](https://github.com/stephane-r/holoplay-wa): Progressive Web App connecting on Invidious API's with search, playlists and favorites. - [WatchTube](https://github.com/WatchTubeTeam/WatchTube): Powerful YouTube client for Apple Watch. - [Yattee](https://github.com/yattee/yattee): Alternative YouTube frontend for iPhone, iPad, Mac and Apple TV. - [TubiTui](https://codeberg.org/777/TubiTui): A lightweight, libre, TUI-based YouTube client. From b66a5c40a97de2816f43b7b6c816f20252eb4cbc Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Wed, 15 Mar 2023 22:37:07 +0100 Subject: [PATCH 079/598] Community: Restore thumbnail qualities array --- src/invidious/channels/community.cr | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/invidious/channels/community.cr b/src/invidious/channels/community.cr index da8be6ea..ce34ff82 100644 --- a/src/invidious/channels/community.cr +++ b/src/invidious/channels/community.cr @@ -77,9 +77,10 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode) json.field "author", author json.field "authorThumbnails" do json.array do + qualities = {32, 48, 76, 100, 176, 512} author_thumbnail = post["authorThumbnail"]["thumbnails"].as_a[0]["url"].as_s - IMAGE_QUALITIES.each do |quality| + qualities.each do |quality| json.object do json.field "url", author_thumbnail.gsub(/s\d+-/, "s#{quality}-") json.field "width", quality From e1a25a184aba09720e29def0b084388088f5c56d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20B=C4=85czek?= Date: Sun, 19 Mar 2023 20:03:15 +0100 Subject: [PATCH 080/598] Add the docs/ folder to gitignore (#3694) --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 1779a73d..7a26e1a6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -/doc/ +/docs/ /dev/ /lib/ /bin/ From a0bdcc29648e6ee886c52c8503885f5860afcb0a Mon Sep 17 00:00:00 2001 From: Matthaiks Date: Wed, 15 Mar 2023 14:28:22 +0000 Subject: [PATCH 081/598] Update Polish translation --- locales/pl.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/locales/pl.json b/locales/pl.json index 2dd3ed87..3ca78e43 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -495,5 +495,7 @@ "channel_tab_shorts_label": "Shorts", "Music in this video": "Muzyka w tym filmie", "Artist: ": "Wykonawca: ", - "Album: ": "Album: " + "Album: ": "Album: ", + "Song: ": "Piosenka: ", + "Channel Sponsor": "Sponsor kanału" } From aad166c96a2ebfd4d4426c8b80d981e422149246 Mon Sep 17 00:00:00 2001 From: Rex_sa Date: Wed, 15 Mar 2023 17:04:57 +0000 Subject: [PATCH 082/598] Update Arabic translation --- locales/ar.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/locales/ar.json b/locales/ar.json index 181ff933..3ce34c2d 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -543,5 +543,7 @@ "channel_tab_channels_label": "القنوات", "Music in this video": "الموسيقى في هذا الفيديو", "Album: ": "الألبوم: ", - "Artist: ": "الفنان: " + "Artist: ": "الفنان: ", + "Song: ": "أغنية: ", + "Channel Sponsor": "راعي القناة" } From 60e3f8aec0424a834e4931dbaa3656a7db893d5d Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Wed, 15 Mar 2023 10:55:03 +0000 Subject: [PATCH 083/598] Update Spanish translation --- locales/es.json | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/locales/es.json b/locales/es.json index a0d16325..bb082c06 100644 --- a/locales/es.json +++ b/locales/es.json @@ -414,8 +414,9 @@ "generic_count_weeks_plural": "{{count}} semanas", "generic_playlists_count": "{{count}} lista de reproducción", "generic_playlists_count_plural": "{{count}} listas de reproducción", - "generic_videos_count": "{{count}} video", - "generic_videos_count_plural": "{{count}} videos", + "generic_videos_count_0": "{{count}} video", + "generic_videos_count_1": "{{count}} videos", + "generic_videos_count_2": "{{count}} videos", "generic_count_months": "{{count}} mes", "generic_count_months_plural": "{{count}} meses", "comments_points_count": "{{count}} punto", @@ -479,5 +480,7 @@ "channel_tab_playlists_label": "Listas de reproducción", "Music in this video": "Música en este video", "Artist: ": "Artista: ", - "Album: ": "Álbum: " + "Album: ": "Álbum: ", + "Song: ": "Canción: ", + "Channel Sponsor": "Patrocinador del canal" } From 72656e802e7227085bbe101ad9ccf79e3c52d389 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Fri, 17 Mar 2023 19:18:25 +0000 Subject: [PATCH 084/598] Update Ukrainian translation --- locales/uk.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/locales/uk.json b/locales/uk.json index b44d237f..4d748e7f 100644 --- a/locales/uk.json +++ b/locales/uk.json @@ -495,5 +495,7 @@ "channel_tab_channels_label": "Канали", "Music in this video": "Музика в цьому відео", "Artist: ": "Виконавець: ", - "Album: ": "Альбом: " + "Album: ": "Альбом: ", + "Song: ": "Пісня: ", + "Channel Sponsor": "Спонсор каналу" } From 46a7be89a738e1c4e8ac110260f0a150ea81b59e Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 16 Mar 2023 03:21:43 +0000 Subject: [PATCH 085/598] Update Chinese (Simplified) translation --- locales/zh-CN.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/locales/zh-CN.json b/locales/zh-CN.json index aff6dd3e..f202cf88 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -463,5 +463,7 @@ "channel_tab_streams_label": "直播", "Album: ": "专辑: ", "channel_tab_shorts_label": "短视频", - "channel_tab_channels_label": "频道" + "channel_tab_channels_label": "频道", + "Song: ": "歌曲: ", + "Channel Sponsor": "频道赞助者" } From dd6c9dbc65b6e5c0169f9e8acbb3b2e09ea3948c Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Thu, 16 Mar 2023 02:25:12 +0000 Subject: [PATCH 086/598] Update Chinese (Traditional) translation --- locales/zh-TW.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 8aa9869a..54090d3d 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -463,5 +463,7 @@ "channel_tab_streams_label": "直播", "Artist: ": "藝術家: ", "Album: ": "專輯: ", - "Music in this video": "此影片中的音樂" + "Music in this video": "此影片中的音樂", + "Channel Sponsor": "頻道贊助者", + "Song: ": "歌曲: " } From ded28b80d33332022f299a12ea1aa49d89e0e388 Mon Sep 17 00:00:00 2001 From: maboroshin Date: Fri, 17 Mar 2023 06:45:19 +0000 Subject: [PATCH 087/598] Update Japanese translation --- locales/ja.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/locales/ja.json b/locales/ja.json index 4d2ed5a0..8a4537d4 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -445,7 +445,7 @@ "search_message_use_another_instance": " 別のインスタンス上でも検索できます。", "search_filters_apply_button": "選択したフィルターを適用", "user_saved_playlists": "`x` 個の保存した再生リスト", - "crash_page_you_found_a_bug": "Invidious でバグを見つけたようです。", + "crash_page_you_found_a_bug": "Invidious のバグのようです!", "crash_page_refresh": "ページを更新を試す", "preferences_watch_history_label": "再生履歴を有効化 ", "search_filters_date_option_none": "すべて", @@ -463,5 +463,7 @@ "channel_tab_channels_label": "チャンネル", "Music in this video": "この動画の音楽", "Artist: ": "アーティスト: ", - "Album: ": "アルバム: " + "Album: ": "アルバム: ", + "Song: ": "曲: ", + "Channel Sponsor": "チャンネルのスポンサー" } From defec2e8fb2b530812b93edac37cb71517b2ca37 Mon Sep 17 00:00:00 2001 From: HamidReza Shareghzade Date: Fri, 17 Mar 2023 08:19:30 +0000 Subject: [PATCH 088/598] Update Persian translation --- locales/fa.json | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/locales/fa.json b/locales/fa.json index fe72a1e8..56685f64 100644 --- a/locales/fa.json +++ b/locales/fa.json @@ -26,15 +26,15 @@ "No": "خیر", "Import and Export Data": "درون‌برد و برون‌برد داده", "Import": "درون‌برد", - "Import Invidious data": "درون‌برد داده اینویدیوس", - "Import YouTube subscriptions": "درون‌برد اشتراک‌های یوتیوب", + "Import Invidious data": "وارد کردن داده JSON اینویدیوس", + "Import YouTube subscriptions": "وارد کردن اشتراک OPML/ یوتیوب", "Import FreeTube subscriptions (.db)": "درون‌برد اشتراک‌های فری‌تیوب (.db)", "Import NewPipe subscriptions (.json)": "درون‌برد اشتراک‌های نیوپایپ (.json)", "Import NewPipe data (.zip)": "درون‌برد داده نیوپایپ (.zip)", "Export": "برون‌برد", "Export subscriptions as OPML": "برون‌برد اشتراک‌ها در قالب OPML", "Export subscriptions as OPML (for NewPipe & FreeTube)": "برون‌برد اشتراک‌ها در قالب OPML (برای نیوپایپ و فری‌تیوب)", - "Export data as JSON": "برون‌برد داده در قالب JSON", + "Export data as JSON": "گرفتن(خارج کردن) اطلاعات اینویدیوس با فرمت JSON", "Delete account?": "حذف حساب کاربری؟", "History": "تاریخچه", "An alternative front-end to YouTube": "یک پیشانه جایگزین برای یوتیوب", @@ -71,7 +71,7 @@ "preferences_related_videos_label": "نمایش ویدیو های مرتبط: ", "preferences_annotations_label": "نمایش حاشیه نویسی ها به طور پیشفرض: ", "preferences_extend_desc_label": "گسترش خودکار توضیحات ویدئو: ", - "preferences_vr_mode_label": "ویدئوها ۳۶۰ درجه تعاملی: ", + "preferences_vr_mode_label": "ویدئوها ۳۶۰ درجه تعاملی(نیازمند WebGL): ", "preferences_category_visual": "ترجیحات بصری", "preferences_player_style_label": "حالت پخش کننده: ", "Dark mode: ": "حالت تاریک: ", @@ -80,7 +80,7 @@ "light": "روشن", "preferences_thin_mode_label": "حالت نازک: ", "preferences_category_misc": "ترجیحات متفرقه", - "preferences_automatic_instance_redirect_label": "هدایت خودکار نمونه (به طور پیش‌فرض به redirect.invidious.io): ", + "preferences_automatic_instance_redirect_label": "هدایت خودکار نمونه (انتقال به redirect.invidious.io): ", "preferences_category_subscription": "ترجیحات اشتراک", "preferences_annotations_subscribed_label": "نمایش حاشیه نویسی ها به طور پیشفرض برای کانال های مشترک شده: ", "Redirect homepage to feed: ": "تغییر مسیر صفحه خانه به خوراک: ", @@ -157,7 +157,7 @@ "Engagement: ": "نامزدی: ", "Whitelisted regions: ": "مناطق لیست سفید: ", "Blacklisted regions: ": "مناطق لیست سیاه: ", - "Shared `x`": "به اشتراک گذاشته شده `x`", + "Shared `x`": "`x` به اشتراک گذاشته شد", "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.": "سلام! مثل اینکه تو جاوا اسکریپت رو خاموش کرده ای. اینجا کلیک کن تا نظرات را ببینی، این رو یادت باشه که ممکنه بارگذاری اونها کمی طول بکشه.", @@ -375,7 +375,7 @@ "next_steps_error_message_refresh": "تازه‌سازی", "next_steps_error_message_go_to_youtube": "رفتن به یوتیوب", "preferences_quality_option_hd720": "HD720", - "preferences_quality_option_dash": "DASH (کیفیت قابل تطبیق)", + "preferences_quality_option_dash": "DASH (کیفیت تطبیفی)", "preferences_quality_option_medium": "میانه", "preferences_quality_option_small": "پایین", "preferences_quality_dash_option_auto": "خودکار", @@ -445,5 +445,10 @@ "Spanish (Spain)": "اسپانیایی (اسپانیا)", "Turkish (auto-generated)": "ترکی (تولید خودکار)", "search_filters_features_option_vr180": "VR180", - "Spanish (Mexico)": "اسپانیایی (مکزیک)" + "Spanish (Mexico)": "اسپانیایی (مکزیک)", + "Popular enabled: ": "محبوب ها فعال شد: ", + "Music in this video": "آهنگ در این ویدیو", + "Artist: ": "هنرمند: ", + "Album: ": "آلبوم: ", + "Song: ": "آهنگ: " } From c1e45cb84a8d117167bf5bd55e92d9e8a4954845 Mon Sep 17 00:00:00 2001 From: Milo Ivir Date: Wed, 15 Mar 2023 18:54:28 +0000 Subject: [PATCH 089/598] Update Croatian translation --- locales/hr.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/locales/hr.json b/locales/hr.json index c626ed28..ade732ad 100644 --- a/locales/hr.json +++ b/locales/hr.json @@ -495,5 +495,7 @@ "channel_tab_shorts_label": "Kratka videa", "Music in this video": "Glazba u ovom videu", "Album: ": "Album: ", - "Artist: ": "Izvođač: " + "Artist: ": "Izvođač: ", + "Channel Sponsor": "Sponzor kanala", + "Song: ": "Pjesma: " } From ce1f61d185dfd817e14e45de1d6ddc59ca09cecf Mon Sep 17 00:00:00 2001 From: Fjuro Date: Wed, 15 Mar 2023 10:50:53 +0000 Subject: [PATCH 090/598] Update Czech translation --- locales/cs.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/locales/cs.json b/locales/cs.json index 51db1550..4611c4fd 100644 --- a/locales/cs.json +++ b/locales/cs.json @@ -495,5 +495,7 @@ "channel_tab_streams_label": "Živé přenosy", "Music in this video": "Hudba v tomto videu", "Artist: ": "Umělec: ", - "Album: ": "Album: " + "Album: ": "Album: ", + "Channel Sponsor": "Sponzor kanálu", + "Song: ": "Skladba: " } From 3aa6a0c4f089c5bddc77d79ec2f30bb7b55242af Mon Sep 17 00:00:00 2001 From: SC Date: Thu, 16 Mar 2023 11:07:12 +0000 Subject: [PATCH 091/598] Update Portuguese translation --- locales/pt.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/locales/pt.json b/locales/pt.json index b6b6c110..310381ae 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -479,5 +479,7 @@ "channel_tab_streams_label": "Diretos", "Music in this video": "Música neste vídeo", "Artist: ": "Artista: ", - "Album: ": "Álbum: " + "Album: ": "Álbum: ", + "Song: ": "Canção: ", + "Channel Sponsor": "Patrocinador do canal" } From c188dec4faf65b23b3a6bbe9028cc0ad0aaa55d9 Mon Sep 17 00:00:00 2001 From: victor dargallo Date: Fri, 17 Mar 2023 21:42:57 +0000 Subject: [PATCH 092/598] Update Catalan translation --- locales/ca.json | 380 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 378 insertions(+), 2 deletions(-) diff --git a/locales/ca.json b/locales/ca.json index 2ba6ae39..c957f561 100644 --- a/locales/ca.json +++ b/locales/ca.json @@ -75,7 +75,7 @@ "Title": "Títol", "Belarusian": "Bielorús", "Enable web notifications": "Activa notificacions web", - "search": "busca", + "search": "cerca", "Catalan": "Català", "Croatian": "Croat", "preferences_category_admin": "Preferències d'administrador", @@ -99,5 +99,381 @@ "Music": "Música", "search_filters_sort_option_relevance": "Rellevància", "search_filters_date_option_hour": "Última hora", - "search_filters_date_option_today": "Avui" + "search_filters_date_option_today": "Avui", + "preferences_volume_label": "Volum del reproductor: ", + "invidious": "Invidious", + "preferences_quality_dash_option_144p": "144p", + "Turkish (auto-generated)": "Turc (generat automàticament)", + "Urdu": "Urdú", + "Vietnamese (auto-generated)": "Vietnamita (generat automàticament)", + "Welsh": "Gal·lès", + "Yoruba": "Ioruba", + "YouTube comment permalink": "Enllaç permanent de comentari de YouTube", + "Channel Sponsor": "Patrocinador del canal", + "Audio mode": "Mode d'àudio", + "search_filters_date_option_none": "Qualsevol data", + "search_filters_type_option_playlist": "Llista de reproducció", + "search_filters_type_option_movie": "Pel·lícula", + "search_filters_features_option_four_k": "4K", + "search_filters_features_option_subtitles": "Subtítols/CC", + "search_filters_features_option_live": "Directe", + "search_filters_features_option_hd": "HD", + "search_filters_features_option_hdr": "HDR", + "search_filters_features_option_location": "Ubicació", + "search_filters_apply_button": "Aplica els filtres seleccionats", + "videoinfo_started_streaming_x_ago": "Ha començat el directe fa `x`", + "next_steps_error_message_go_to_youtube": "Anar a YouTube", + "footer_donate_page": "Donar", + "footer_original_source_code": "Codi font original", + "videoinfo_watch_on_youTube": "Veure a YouTube", + "user_saved_playlists": "`x` llistes de reproducció guardades", + "adminprefs_modified_source_code_url_label": "URL al repositori de codi font modificat", + "none": "cap", + "footer_modfied_source_code": "Codi font modificat", + "videoinfo_invidious_embed_link": "Incrusta l'enllaç", + "download_subtitles": "Subtítols - `x` (.vtt)", + "user_created_playlists": "`x`llistes de reproducció creades", + "Video unavailable": "Vídeo no disponible", + "channel_tab_channels_label": "Canals", + "channel_tab_playlists_label": "Llistes de reproducció", + "channel_tab_community_label": "Comunitat", + "Invalid TFA code": "Codi TFA no vàlid", + "Czech": "Txec", + "Default": "Per defecte", + "Amharic": "Amàric", + "preferences_automatic_instance_redirect_label": "Redirecció automàtica d'instàncies (retorna a redirect.invidious.io): ", + "Login enabled: ": "Activa inici de sessió: ", + "Registration enabled: ": "Activa registre: ", + "Whitelisted regions: ": "Regions a la llista blanca: ", + "Chinese (Simplified)": "Xinès (Simplificat)", + "Corsican": "Cors", + "Estonian": "Estonià", + "Japanese (auto-generated)": "Japonès (generat automàticament)", + "English (United States)": "Anglès (Estats Units)", + "English (auto-generated)": "Anglès (generat automàticament)", + "Cebuano": "Cebuà", + "Esperanto": "Esperanto", + "Scottish Gaelic": "Gaèlic escocès", + "Playlists": "Llistes de reproducció", + "search_filters_title": "Filtres", + "search_filters_type_option_all": "Qualsevol tipus", + "search_filters_duration_option_none": "Qualsevol duració", + "next_steps_error_message": "Després d'això, hauríeu d'intentar: ", + "next_steps_error_message_refresh": "Recarregar la pàgina", + "crash_page_refresh": "ha intentat actualitzar la pàgina", + "crash_page_report_issue": "Si cap de les anteriors no ha ajudat, obre un nou issue a GitHub (preferiblement en anglès) i inclou el text següent al missatge (NO tradueixis aquest text):", + "generic_subscriptions_count": "{{count}} subscripció", + "generic_subscriptions_count_plural": "{{count}} subscripcions", + "error_video_not_in_playlist": "El vídeo sol·licitat no existeix en aquesta llista de reproducció. Fes clic aquí per a la pàgina d'inici de la llista de reproducció.", + "comments_points_count": "{{count}} punt", + "comments_points_count_plural": "{{count}} punts", + "%A %B %-d, %Y": "%A %B %-d, %Y", + "Create playlist": "Crear llista de reproducció", + "Text CAPTCHA": "Text CAPTCHA", + "Next page": "Pàgina següent", + "preferences_category_visual": "Preferències visuals", + "preferences_unseen_only_label": "Mostra només no vistos: ", + "preferences_listen_label": "Escolta per defecte: ", + "Import": "Importar", + "Token": "Senyal", + "Wilson score: ": "Puntuació de Wilson: ", + "search_filters_date_label": "Data de càrrega", + "search_filters_features_option_three_sixty": "360°", + "source": "font", + "preferences_default_home_label": "Pàgina d'inici per defecte: ", + "preferences_comments_label": "Comentaris per defecte: ", + "`x` uploaded a video": "`x` ha penjat un vídeo", + "Released under the AGPLv3 on Github.": "Publicat sota l'AGPLv3 a GitHub.", + "Token manager": "Gestor de tokens", + "Watch history": "Historial de reproduccions", + "Cannot change password for Google accounts": "No es pot canviar la contrasenya dels comptes de Google", + "Authorize token?": "Autoritzar senyal?", + "Source available here.": "Font disponible aquí.", + "Export subscriptions as OPML (for NewPipe & FreeTube)": "Exporta subscripcions com a OPML (per a NewPipe i FreeTube)", + "Log in": "Inicia sessió", + "search_filters_sort_option_date": "Data de càrrega", + "Unlisted": "No llistat", + "View privacy policy.": "Veure política de privadesa.", + "Public": "Públic", + "View all playlists": "Veure totes les llistes de reproducció", + "reddit": "Reddit", + "Manage tokens": "Gestiona senyals", + "Not a playlist.": "No és una llista de reproducció.", + "preferences_local_label": "Vídeos de Proxy: ", + "View channel on YouTube": "Veure canal a Youtube", + "preferences_quality_dash_option_1080p": "1080p", + "Top enabled: ": "Activa top: ", + "Delete playlist `x`?": "Eliminar llista de reproducció `x`?", + "View JavaScript license information.": "Consulta la informació de la llicència de JavaScript.", + "Playlist privacy": "Privacitat de la llista de reproducció", + "search_message_no_results": "No s'han trobat resultats.", + "search_message_use_another_instance": " També es pot buscar en una altra instància.", + "Genre: ": "Gènere: ", + "Hidden field \"challenge\" is a required field": "El camp ocult \"repte\" és un camp obligatori", + "Burmese": "Birmà", + "View as playlist": "Mostra com a llista de reproducció", + "preferences_category_subscription": "Preferències de subscripció", + "Music in this video": "Música en aquest vídeo", + "Artist: ": "Artista: ", + "Album: ": "Àlbum: ", + "Shared `x`": "Compartit `x`", + "Premieres `x`": "Estrena `x`", + "View more comments on Reddit": "Veure més comentaris a Reddit", + "View `x` comments": { + "([^.,0-9]|^)1([^.,0-9]|$)": "Veure `x` comentari", + "": "Veure `x` comentaris" + }, + "View Reddit comments": "Veure comentaris de Reddit", + "Incorrect password": "Contrasenya incorrecta", + "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "No es pot iniciar la sessió, assegureu-vos que l'autenticació de dos factors (Autenticador o SMS) estigui activada.", + "Erroneous CAPTCHA": "CAPTCHA erroni", + "CAPTCHA is a required field": "El CAPTCHA és un camp obligatori", + "Korean (auto-generated)": "Coreà (generat automàticament)", + "Kyrgyz": "Kirguís", + "Latin": "Llatí", + "Malagasy": "Malgaix", + "Maori": "Maori", + "Marathi": "Marathi", + "Norwegian Bokmål": "Bokmål Noruec", + "Nyanja": "Nyanja", + "Portuguese (Brazil)": "Portuguès (Brazil)", + "Punjabi": "Panjabi", + "Russian (auto-generated)": "Rus (generat automàticament)", + "Samoan": "Samoà", + "Somali": "Somali", + "Southern Sotho": "Sesotho", + "Spanish (Mexico)": "Espanyol (Mèxic)", + "Spanish (Spain)": "Espanyol (Espanya)", + "Sundanese": "Sondanès", + "Swahili": "Suahili", + "Tamil": "Tàmil", + "Telugu": "Telugu", + "Zulu": "Zulu", + "generic_count_months": "{{count}} mes", + "generic_count_months_plural": "{{count}} mesos", + "generic_count_weeks": "{{count}} setmana", + "generic_count_weeks_plural": "{{count}} setmanes", + "About": "Sobre", + "`x` marked it with a ❤": "`x`marca'l amb un ❤", + "Video mode": "Mode de vídeo", + "search_filters_features_label": "Característiques", + "search_filters_features_option_c_commons": "Creative Commons", + "search_filters_features_option_vr180": "VR180", + "search_filters_features_option_three_d": "3D", + "search_filters_features_option_purchased": "Comprat", + "Chinese (Hong Kong)": "Xinès (Hong Kong)", + "Chinese (Taiwan)": "Xinès (Taiwan)", + "Hmong": "Hmong", + "Kazakh": "Kazakh", + "Igbo": "Igbo", + "Javanese": "Javanès", + "Indonesian (auto-generated)": "Indonesi (generat automàticament)", + "Interlingue": "Interlingüe", + "Khmer": "Khmer", + "This channel does not exist.": "Aquest canal no existeix.", + "Song: ": "Cançó: ", + "Login failed. This may be because two-factor authentication is not turned on for your account.": "Error a l'iniciar sessió. Això pot ser perquè l'autenticació de dos factors no està activada per al vostre compte.", + "channel:`x`": "canal: `x`", + "Deleted or invalid channel": "Canal suprimit o no vàlid", + "Could not get channel info.": "No s'ha pogut obtenir la informació del canal.", + "Could not pull trending pages.": "No s'han pogut extreure les pàgines de tendència.", + "comments_view_x_replies": "Veure {{count}} resposta", + "comments_view_x_replies_plural": "Veure {{count}} respostes", + "Subscriptions": "Subscripcions", + "generic_count_seconds": "{{count}} segon", + "generic_count_seconds_plural": "{{count}} segons", + "channel_tab_shorts_label": "Vídeos curts", + "preferences_save_player_pos_label": "Desa la posició de reproducció: ", + "crash_page_before_reporting": "Abans d'informar d'un error, assegureu-vos que teniu:", + "crash_page_switch_instance": "ha intentat utilitzar una altra instància", + "crash_page_read_the_faq": "heu llegit les Preguntes més freqüents (FAQ)", + "crash_page_search_issue": "ha cercat problemes existents a GitHub", + "User ID is a required field": "L'identificador d'usuari és un camp obligatori", + "Password is a required field": "La contrasenya és un camp obligatori", + "Wrong username or password": "Nom d'usuari o contrasenya incorrectes", + "Please sign in using 'Log in with Google'": "Si us plau, inicieu la sessió amb 'Inicieu sessió amb Google'", + "Password cannot be longer than 55 characters": "La contrasenya no pot tenir més de 55 caràcters", + "Invidious Private Feed for `x`": "Feed privat Invidious per a `x`", + "generic_views_count": "{{count}} visualització", + "generic_views_count_plural": "{{count}} visualitzacions", + "generic_videos_count": "{{count}} vídeo", + "generic_videos_count_plural": "{{count}} vídeos", + "Token is expired, please try again": "La senyal ha caducat, torna-ho a provar", + "English": "Anglès", + "Kannada": "Kanarès", + "Erroneous token": "Senyal errònia", + "`x` ago": "fa `x`", + "Empty playlist": "Llista de reproducció buida", + "Playlist does not exist.": "La llista de reproducció no existeix.", + "No such user": "No hi ha tal usuari", + "Afrikaans": "Afrikàans", + "Azerbaijani": "Azerbaidjana", + "Cantonese (Hong Kong)": "Cantonès (Hong Kong)", + "Chinese": "Xinès", + "Chinese (China)": "Xinès (Xina)", + "Chinese (Traditional)": "Xinès (Tradicional)", + "Dutch": "Holandès", + "Dutch (auto-generated)": "Holandès (generat automàticament)", + "French (auto-generated)": "Francès (generat automàticament)", + "Georgian": "Georgià", + "German (auto-generated)": "Alemany (generat automàticament)", + "Gujarati": "Gujarati", + "Hawaiian": "Hawaià", + "generic_count_years": "{{count}} any", + "generic_count_years_plural": "{{count}} anys", + "Popular": "Popular", + "Rating: ": "Valoració: ", + "permalink": "enllaç permanent", + "preferences_quality_dash_option_worst": "Pitjor", + "Yiddish": "Ídix", + "preferences_quality_dash_option_auto": "Automàtic", + "Western Frisian": "Frisó occidental", + "Swedish": "Suec", + "Only show latest unwatched video from channel: ": "Mostra només l'últim vídeo no vist del canal: ", + "preferences_continue_label": "Reprodueix el següent per defecte: ", + "Import YouTube subscriptions": "Importar subscripcions de YouTube", + "search_filters_sort_option_rating": "Valoració", + "preferences_thin_mode_label": "Mode prim: ", + "preferences_quality_option_small": "Petit", + "CAPTCHA enabled: ": "activa CAPTCHA: ", + "Import and Export Data": "Importar i exportar dades", + "preferences_quality_dash_option_360p": "360p", + "Popular enabled: ": "Activa popular: ", + "Password": "Contrasenya", + "Blacklisted regions: ": "Regions a la llista negra: ", + "Register": "Registra't", + "Shared `x` ago": "Compartit fa `x`", + "search_filters_sort_option_views": "Recompte de visualitzacions", + "Import Invidious data": "Importa dades JSON d'Invidious", + "preferences_related_videos_label": "Mostra vídeos relacionats: ", + "preferences_show_nick_label": "Mostra l'àlies a la part superior: ", + "Time (h:mm:ss):": "Temps (h:mm:ss):", + "Could not fetch comments": "No s'han pogut obtenir els comentaris", + "New password": "Nova contrasenya", + "preferences_notifications_only_label": "Mostra només notificacions (si n'hi ha): ", + "preferences_annotations_label": "Mostra anotacions per defecte: ", + "Import FreeTube subscriptions (.db)": "Importar subscripcions de FreeTube (.db)", + "Fallback captions: ": "Subtítols alternatius: ", + "Log out": "Tancar sessió", + "preferences_quality_dash_option_2160p": "2160p", + "Unsubscribe": "Cancel·la la subscripció", + "Log in/register": "Inicia sessió/registra't", + "Nepali": "Nepalí", + "Xhosa": "Xosa", + "preferences_captions_label": "Subtítols per defecte: ", + "preferences_autoplay_label": "Reproducció automàtica: ", + "`x` is live": "`x` està en directe", + "Uzbek": "Uzbek", + "Hausa": "Haussa", + "Bosnian": "Bosnià", + "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Hola! Sembla que tens JavaScript desactivat. Feu clic aquí per veure els comentaris, tingueu en compte que poden trigar una mica més a carregar-se.", + "Password cannot be empty": "La contrasenya no pot estar buida", + "preferences_video_loop_label": "Sempre en bucle: ", + "preferences_quality_option_dash": "DASH (qualitat adaptativa)", + "Change password": "Canvia la contrasenya", + "Export data as JSON": "Exporta dades d'Invidious com a JSON", + "Wrong answer": "Resposta incorrecta", + "Clear watch history": "Neteja l'historial de reproduccions", + "Mongolian": "Mongol", + "preferences_quality_dash_option_best": "Millor", + "Authorize token for `x`?": "Autoritzar senyal per a `x`?", + "Report statistics: ": "Estadístiques de l'informe: ", + "Switch Invidious Instance": "Canvia la instància d'Invidious", + "History": "Historial", + "Portuguese (auto-generated)": "Portuguès (generat automàticament)", + "footer_source_code": "Codi font", + "videoinfo_youTube_embed_link": "Insereix", + "generic_count_minutes": "{{count}} minut", + "generic_count_minutes_plural": "{{count}} minuts", + "preferences_category_player": "Preferències del reproductor", + "Sign In": "Inicia Sessió", + "preferences_continue_autoplay_label": "Reprodueix automàticament el següent vídeo: ", + "generic_playlists_count": "{{count}} llista de reproducció", + "generic_playlists_count_plural": "{{count}} llistes de reproducció", + "Delete account?": "Esborrar compte?", + "Please log in": "Si us plau inicieu sessió", + "Import NewPipe data (.zip)": "Importar dades de NewPipe (.zip)", + "Image CAPTCHA": "Imatge CAPTCHA", + "channel_tab_streams_label": "Transmissions en directe", + "preferences_category_misc": "Preferències diverses", + "preferences_annotations_subscribed_label": "Mostra les anotacions per defecte dels canals subscrits? ", + "Tajik": "Tadjik", + "preferences_player_style_label": "Estil del reproductor: ", + "Load more": "Carrega més", + "preferences_vr_mode_label": "Vídeos interactius de 360 graus (requereix WebGL): ", + "Manage subscriptions": "Gestionar les subscripcions", + "preferences_quality_option_medium": "Mitjà", + "Editing playlist `x`": "Editant la llista de reproducció `x`", + "search_filters_duration_option_medium": "Mitjà (4 - 20 minuts)", + "E-mail": "Correu electrònic", + "Spanish (auto-generated)": "Castellà (generat automàticament)", + "Export": "Exportar", + "preferences_quality_dash_option_4320p": "4320p", + "JavaScript license information": "Informació de la llicència de JavaScript", + "Hidden field \"token\" is a required field": "El camp ocult \"senyal\" és un camp obligatori", + "Shona": "Xona", + "Family friendly? ": "Apte per a tots els públics? ", + "preferences_quality_dash_label": "Qualitat de vídeo DASH preferida: ", + "Hindi": "Hindi", + "An alternative front-end to YouTube": "Una interfície alternativa a YouTube", + "Export subscriptions as OPML": "Exporta subscripcions com a OPML", + "Watch on YouTube": "Veure a YouTube", + "Lao": "Laosià", + "search_message_change_filters_or_query": "Proveu d'ampliar la vostra consulta de cerca i/o canviar els filtres.", + "View YouTube comments": "Veure comentaris de YouTube", + "New passwords must match": "Les contrasenyes noves han de coincidir", + "Subscription manager": "Gestor de subscripcions", + "Premieres in `x`": "Estrena en `x`", + "youtube": "YouTube", + "Latvian": "Letó", + "LIVE": "EN VIU", + "Could not create mix.": "No s'ha pogut crear la barreja.", + "preferences_speed_label": "Velocitat per defecte: ", + "preferences_extend_desc_label": "Amplieu automàticament la descripció del vídeo: ", + "popular": "popular", + "Erroneous challenge": "Repte erroni", + "last": "darrer", + "preferences_quality_dash_option_240p": "240p", + "preferences_quality_dash_option_720p": "720p", + "preferences_quality_dash_option_480p": "480p", + "Log in with Google": "Inicia sessió amb Google", + "preferences_quality_dash_option_1440p": "1440p", + "Previous page": "Pàgina anterior", + "Only show latest video from channel: ": "Mostra només l'últim vídeo del canal: ", + "unsubscribe": "cancel·la la subscripció", + "View playlist on YouTube": "Veure llista de reproducció a YouTube", + "Import NewPipe subscriptions (.json)": "Importar subscripcions de NewPipe (.json)", + "crash_page_you_found_a_bug": "Sembla que has trobat un error a Invidious!", + "Subscribe": "Subscriu-me", + "Quota exceeded, try again in a few hours": "S'ha superat la quota, torna-ho a provar d'aquí a unes hores", + "generic_count_days": "{{count}} dia", + "generic_count_days_plural": "{{count}} dies", + "Trending": "Tendència", + "Updated `x` ago": "Actualitzat fa `x`", + "Haitian Creole": "Crioll Haitià", + "preferences_watch_history_label": "Habilita historial de reproduccions: ", + "generic_count_hours": "{{count}} hora", + "generic_count_hours_plural": "{{count}} hores", + "Malayalam": "Maialàiam", + "Clear watch history?": "Neteja historial de reproduccions?", + "Import/export data": "Importa/exporta dades", + "Sinhala": "Singalès", + "Delete playlist": "Eliminar llista de reproducció", + "Bangla": "Bengalí", + "Italian (auto-generated)": "Italià (generat automàticament)", + "License: ": "Llicència: ", + "(edited)": "(editat)", + "Pashto": "Paixtu", + "preferences_dark_mode_label": "Tema: ", + "revoke": "revocar", + "English (United Kingdom)": "Anglès (Regne Unit)", + "preferences_quality_option_hd720": "HD720", + "tokens_count": "{{count}} senyal", + "tokens_count_plural": "{{count}} senyals", + "subscriptions_unseen_notifs_count": "{{count}} notificació no vista", + "subscriptions_unseen_notifs_count_plural": "{{count}} notificacions no vistes", + "generic_subscribers_count": "{{count}} subscriptor", + "generic_subscribers_count_plural": "{{count}} subscriptors", + "Sindhi": "Sindhi", + "Slovenian": "Eslovè" } From 224fbcd2b1109e1719be7a8590dc816ab5f06bf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?O=C4=9Fuz=20Ersen?= Date: Sat, 18 Mar 2023 18:22:51 +0000 Subject: [PATCH 093/598] Update Turkish translation --- locales/tr.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/locales/tr.json b/locales/tr.json index b7cb3958..6e0bc175 100644 --- a/locales/tr.json +++ b/locales/tr.json @@ -479,5 +479,7 @@ "channel_tab_playlists_label": "Oynatma Listeleri", "Album: ": "Albüm: ", "Music in this video": "Bu videodaki müzik", - "Artist: ": "Sanatçı: " + "Artist: ": "Sanatçı: ", + "Channel Sponsor": "Kanal Sponsoru", + "Song: ": "Şarkı: " } From 08cbd44b57b8993e66d9dc93b22e4801f208ad9b Mon Sep 17 00:00:00 2001 From: victor dargallo Date: Sat, 18 Mar 2023 19:59:07 +0000 Subject: [PATCH 094/598] Update Catalan translation --- locales/ca.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/locales/ca.json b/locales/ca.json index c957f561..54a0b177 100644 --- a/locales/ca.json +++ b/locales/ca.json @@ -475,5 +475,11 @@ "generic_subscribers_count": "{{count}} subscriptor", "generic_subscribers_count_plural": "{{count}} subscriptors", "Sindhi": "Sindhi", - "Slovenian": "Eslovè" + "Slovenian": "Eslovè", + "preferences_feed_menu_label": "Menú del feed: ", + "Fallback comments: ": "Comentaris alternatius: ", + "Top": "Millors", + "preferences_max_results_label": "Nombre de vídeos mostrats al feed: ", + "Engagement: ": "Atracció: ", + "Redirect homepage to feed: ": "Redirigeix la pàgina d'inici al feed: " } From 1f3317e257745a7ef4c44fcf8748ec18fe401fb0 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Mon, 27 Feb 2023 21:55:28 +0100 Subject: [PATCH 095/598] Update video spec --- mocks | 2 +- .../videos/regular_videos_extract_spec.cr | 36 ++--- .../videos/scheduled_live_extract_spec.cr | 148 ++---------------- 3 files changed, 33 insertions(+), 153 deletions(-) diff --git a/mocks b/mocks index dfd53ea6..cb16e034 160000 --- a/mocks +++ b/mocks @@ -1 +1 @@ -Subproject commit dfd53ea6ceb3cbcbbce6004f6ce60b330ad0f9b1 +Subproject commit cb16e0343c8f94182615610bfe3c503db89717a7 diff --git a/spec/invidious/videos/regular_videos_extract_spec.cr b/spec/invidious/videos/regular_videos_extract_spec.cr index 132b37a3..cbe80010 100644 --- a/spec/invidious/videos/regular_videos_extract_spec.cr +++ b/spec/invidious/videos/regular_videos_extract_spec.cr @@ -17,8 +17,8 @@ Spectator.describe "parse_video_info" do # Basic video infos expect(info["title"].as_s).to eq("I Gave My 100,000,000th Subscriber An Island") - expect(info["views"].as_i).to eq(32_846_329) - expect(info["likes"].as_i).to eq(2_611_650) + expect(info["views"].as_i).to eq(115_784_415) + expect(info["likes"].as_i).to eq(4_932_790) # For some reason the video length from VideoDetails and the # one from microformat differs by 1s... @@ -46,14 +46,14 @@ Spectator.describe "parse_video_info" do # Related videos - expect(info["relatedVideos"].as_a.size).to eq(19) + expect(info["relatedVideos"].as_a.size).to eq(20) - expect(info["relatedVideos"][0]["id"]).to eq("tVWWp1PqDus") - expect(info["relatedVideos"][0]["title"]).to eq("100 Girls Vs 100 Boys For $500,000") + expect(info["relatedVideos"][0]["id"]).to eq("iogcY_4xGjo") + expect(info["relatedVideos"][0]["title"]).to eq("$1 vs $1,000,000 Hotel Room!") expect(info["relatedVideos"][0]["author"]).to eq("MrBeast") expect(info["relatedVideos"][0]["ucid"]).to eq("UCX6OQ3DkcsbYNE6H8uQQuVA") - expect(info["relatedVideos"][0]["view_count"]).to eq("49702799") - expect(info["relatedVideos"][0]["short_view_count"]).to eq("49M") + expect(info["relatedVideos"][0]["view_count"]).to eq("172972109") + expect(info["relatedVideos"][0]["short_view_count"]).to eq("172M") expect(info["relatedVideos"][0]["author_verified"]).to eq("true") # Description @@ -76,11 +76,11 @@ Spectator.describe "parse_video_info" do expect(info["ucid"].as_s).to eq("UCX6OQ3DkcsbYNE6H8uQQuVA") expect(info["authorThumbnail"].as_s).to eq( - "https://yt3.ggpht.com/ytc/AMLnZu84dsnlYtuUFBMC8imQs0IUcTKA9khWAmUOgQZltw=s48-c-k-c0x00ffffff-no-rj" + "https://yt3.ggpht.com/ytc/AL5GRJUfhQdJS6n-YJtsAf-ouS2myDavDOq_zXBfebal3Q=s48-c-k-c0x00ffffff-no-rj" ) expect(info["authorVerified"].as_bool).to be_true - expect(info["subCountText"].as_s).to eq("101M") + expect(info["subCountText"].as_s).to eq("135M") end it "parses a regular video with no descrition/comments" do @@ -99,7 +99,7 @@ Spectator.describe "parse_video_info" do # Basic video infos expect(info["title"].as_s).to eq("Chris Rea - Auberge") - expect(info["views"].as_i).to eq(10_356_197) + expect(info["views"].as_i).to eq(10_698_554) expect(info["likes"].as_i).to eq(0) expect(info["lengthSeconds"].as_i).to eq(283_i64) expect(info["published"].as_s).to eq("2012-05-21T00:00:00Z") @@ -132,16 +132,14 @@ Spectator.describe "parse_video_info" do # Related videos - expect(info["relatedVideos"].as_a.size).to eq(19) + expect(info["relatedVideos"].as_a.size).to eq(18) - expect(info["relatedVideos"][0]["id"]).to eq("0bkrY_V0yZg") - expect(info["relatedVideos"][0]["title"]).to eq( - "Chris Rea Best Songs Collection - Chris Rea Greatest Hits Full Album 2022" - ) - expect(info["relatedVideos"][0]["author"]).to eq("Rock Ultimate") - expect(info["relatedVideos"][0]["ucid"]).to eq("UCekSc2A19di9koUIpj8gxlQ") - expect(info["relatedVideos"][0]["view_count"]).to eq("1992412") - expect(info["relatedVideos"][0]["short_view_count"]).to eq("1.9M") + expect(info["relatedVideos"][0]["id"]).to eq("rfyZrJUmzxU") + expect(info["relatedVideos"][0]["title"]).to eq("cheb mami - bekatni") + expect(info["relatedVideos"][0]["author"]).to eq("pelitovic") + expect(info["relatedVideos"][0]["ucid"]).to eq("UCsp6vFyJeGoLxgn-AsHp1tw") + expect(info["relatedVideos"][0]["view_count"]).to eq("13863619") + expect(info["relatedVideos"][0]["short_view_count"]).to eq("13M") expect(info["relatedVideos"][0]["author_verified"]).to eq("false") # Description diff --git a/spec/invidious/videos/scheduled_live_extract_spec.cr b/spec/invidious/videos/scheduled_live_extract_spec.cr index ff5aacd5..9dd22b97 100644 --- a/spec/invidious/videos/scheduled_live_extract_spec.cr +++ b/spec/invidious/videos/scheduled_live_extract_spec.cr @@ -1,114 +1,13 @@ require "../../parsers_helper.cr" Spectator.describe "parse_video_info" do - it "parses scheduled livestreams data (test 1)" do - # Enable mock - _player = load_mock("video/scheduled_live_nintendo.player") - _next = load_mock("video/scheduled_live_nintendo.next") - - raw_data = _player.merge!(_next) - info = parse_video_info("QMGibBzTu0g", raw_data) - - # Some basic verifications - expect(typeof(info)).to eq(Hash(String, JSON::Any)) - - expect(info["videoType"].as_s).to eq("Scheduled") - - # Basic video infos - - expect(info["title"].as_s).to eq("Xenoblade Chronicles 3 Nintendo Direct") - expect(info["views"].as_i).to eq(160) - expect(info["likes"].as_i).to eq(2_283) - expect(info["lengthSeconds"].as_i).to eq(0_i64) - expect(info["published"].as_s).to eq("2022-06-22T14:00:00Z") # Unix 1655906400 - - # Extra video infos - - expect(info["allowedRegions"].as_a).to_not be_empty - expect(info["allowedRegions"].as_a.size).to eq(249) - - expect(info["allowedRegions"].as_a).to contain( - "AD", "BA", "BB", "BW", "BY", "EG", "GG", "HN", "NP", "NR", "TR", - "TT", "TV", "TW", "TZ", "VA", "VC", "VE", "VG", "VI", "VN", "VU", - "WF", "WS", "YE", "YT", "ZA", "ZM", "ZW" - ) - - expect(info["keywords"].as_a).to_not be_empty - expect(info["keywords"].as_a.size).to eq(11) - - expect(info["keywords"].as_a).to contain_exactly( - "nintendo", - "game", - "gameplay", - "fun", - "video game", - "action", - "adventure", - "rpg", - "play", - "switch", - "nintendo switch" - ).in_any_order - - expect(info["allowRatings"].as_bool).to be_true - expect(info["isFamilyFriendly"].as_bool).to be_true - expect(info["isListed"].as_bool).to be_true - expect(info["isUpcoming"].as_bool).to be_true - - # Related videos - - expect(info["relatedVideos"].as_a.size).to eq(20) - - # related video #1 - expect(info["relatedVideos"][3]["id"].as_s).to eq("a-SN3lLIUEo") - expect(info["relatedVideos"][3]["author"].as_s).to eq("Nintendo") - expect(info["relatedVideos"][3]["ucid"].as_s).to eq("UCGIY_O-8vW4rfX98KlMkvRg") - expect(info["relatedVideos"][3]["view_count"].as_s).to eq("147796") - expect(info["relatedVideos"][3]["short_view_count"].as_s).to eq("147K") - expect(info["relatedVideos"][3]["author_verified"].as_s).to eq("true") - - # Related video #2 - expect(info["relatedVideos"][16]["id"].as_s).to eq("l_uC1jFK0lo") - expect(info["relatedVideos"][16]["author"].as_s).to eq("Nintendo") - expect(info["relatedVideos"][16]["ucid"].as_s).to eq("UCGIY_O-8vW4rfX98KlMkvRg") - expect(info["relatedVideos"][16]["view_count"].as_s).to eq("53510") - expect(info["relatedVideos"][16]["short_view_count"].as_s).to eq("53K") - expect(info["relatedVideos"][16]["author_verified"].as_s).to eq("true") - - # Description - - description = "Tune in on 6/22 at 7 a.m. PT for a livestreamed Xenoblade Chronicles 3 Direct presentation featuring roughly 20 minutes of information about the upcoming RPG adventure for Nintendo Switch." - - expect(info["description"].as_s).to eq(description) - expect(info["shortDescription"].as_s).to eq(description) - expect(info["descriptionHtml"].as_s).to eq(description) - - # Video metadata - - expect(info["genre"].as_s).to eq("Gaming") - expect(info["genreUcid"].as_s).to be_empty - expect(info["license"].as_s).to be_empty - - # Author infos - - expect(info["author"].as_s).to eq("Nintendo") - expect(info["ucid"].as_s).to eq("UCGIY_O-8vW4rfX98KlMkvRg") - - expect(info["authorThumbnail"].as_s).to eq( - "https://yt3.ggpht.com/ytc/AKedOLTt4vtjREUUNdHlyu9c4gtJjG90M9jQheRlLKy44A=s48-c-k-c0x00ffffff-no-rj" - ) - - expect(info["authorVerified"].as_bool).to be_true - expect(info["subCountText"].as_s).to eq("8.5M") - end - - it "parses scheduled livestreams data (test 2)" do + it "parses scheduled livestreams data" do # Enable mock _player = load_mock("video/scheduled_live_PBD-Podcast.player") _next = load_mock("video/scheduled_live_PBD-Podcast.next") raw_data = _player.merge!(_next) - info = parse_video_info("RG0cjYbXxME", raw_data) + info = parse_video_info("N-yVic7BbY0", raw_data) # Some basic verifications expect(typeof(info)).to eq(Hash(String, JSON::Any)) @@ -117,11 +16,11 @@ Spectator.describe "parse_video_info" do # Basic video infos - expect(info["title"].as_s).to eq("The Truth About Greenpeace w/ Dr. Patrick Moore | PBD Podcast | Ep. 171") - expect(info["views"].as_i).to eq(24) - expect(info["likes"].as_i).to eq(22) + expect(info["title"].as_s).to eq("Home Team | PBD Podcast | Ep. 241") + expect(info["views"].as_i).to eq(6) + expect(info["likes"].as_i).to eq(7) expect(info["lengthSeconds"].as_i).to eq(0_i64) - expect(info["published"].as_s).to eq("2022-07-14T13:00:00Z") # Unix 1657803600 + expect(info["published"].as_s).to eq("2023-02-28T14:00:00Z") # Unix 1677592800 # Extra video infos @@ -173,39 +72,22 @@ Spectator.describe "parse_video_info" do expect(info["relatedVideos"].as_a.size).to eq(20) - # related video #1 - expect(info["relatedVideos"][2]["id"]).to eq("La9oLLoI5Rc") - expect(info["relatedVideos"][2]["author"]).to eq("Tom Bilyeu") - expect(info["relatedVideos"][2]["ucid"]).to eq("UCnYMOamNKLGVlJgRUbamveA") - expect(info["relatedVideos"][2]["view_count"]).to eq("13329149") - expect(info["relatedVideos"][2]["short_view_count"]).to eq("13M") - expect(info["relatedVideos"][2]["author_verified"]).to eq("true") - - # Related video #2 - expect(info["relatedVideos"][9]["id"]).to eq("IQ_4fvpzYuA") - expect(info["relatedVideos"][9]["author"]).to eq("Business Today") - expect(info["relatedVideos"][9]["ucid"]).to eq("UCaPHWiExfUWaKsUtENLCv5w") - expect(info["relatedVideos"][9]["view_count"]).to eq("26432") - expect(info["relatedVideos"][9]["short_view_count"]).to eq("26K") - expect(info["relatedVideos"][9]["author_verified"]).to eq("true") + expect(info["relatedVideos"][0]["id"]).to eq("j7jPzzjbVuk") + expect(info["relatedVideos"][0]["author"]).to eq("Democracy Now!") + expect(info["relatedVideos"][0]["ucid"]).to eq("UCzuqE7-t13O4NIDYJfakrhw") + expect(info["relatedVideos"][0]["view_count"]).to eq("7576") + expect(info["relatedVideos"][0]["short_view_count"]).to eq("7.5K") + expect(info["relatedVideos"][0]["author_verified"]).to eq("true") # Description - description_start_text = <<-TXT - PBD Podcast Episode 171. In this episode, Patrick Bet-David is joined by Dr. Patrick Moore and Adam Sosnick. - - Join the channel to get exclusive access to perks: https://bit.ly/3Q9rSQL - TXT + description_start_text = "PBD Podcast Episode 241. The home team is ready and at it again with the latest news, interesting topics and trending conversations on topics that matter. Try our sponsor Aura for 14 days free - https://aura.com/pbd" expect(info["description"].as_s).to start_with(description_start_text) expect(info["shortDescription"].as_s).to start_with(description_start_text) expect(info["descriptionHtml"].as_s).to start_with( - <<-TXT - PBD Podcast Episode 171. In this episode, Patrick Bet-David is joined by Dr. Patrick Moore and Adam Sosnick. - - Join the channel to get exclusive access to perks: bit.ly/3Q9rSQL - TXT + "PBD Podcast Episode 241. The home team is ready and at it again with the latest news, interesting topics and trending conversations on topics that matter. Try our sponsor Aura for 14 days free - aura.com/pbd" ) # Video metadata @@ -223,6 +105,6 @@ Spectator.describe "parse_video_info" do "https://yt3.ggpht.com/61ArDiQshJrvSXcGLhpFfIO3hlMabe2fksitcf6oGob0Mdr5gztdkXxRljICUodL4iuTSrtxW4A=s48-c-k-c0x00ffffff-no-rj" ) expect(info["authorVerified"].as_bool).to be_false - expect(info["subCountText"].as_s).to eq("227K") + expect(info["subCountText"].as_s).to eq("594K") end end From 4ae158ef6dcb89c2cd0eec646a42f11ebc207fba Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sun, 19 Mar 2023 22:44:59 +0100 Subject: [PATCH 096/598] Videos: Add back support for views on livestreams --- src/invidious/videos/parser.cr | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr index 04ee7303..efc4b2e5 100644 --- a/src/invidious/videos/parser.cr +++ b/src/invidious/videos/parser.cr @@ -185,10 +185,12 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any # We have to try to extract viewCount from videoPrimaryInfoRenderer first, # then from videoDetails, as the latter is "0" for livestreams (we want # to get the amount of viewers watching). - views_txt = video_primary_renderer - .try &.dig?("viewCount", "videoViewCountRenderer", "viewCount", "simpleText") - views_txt ||= video_details["viewCount"]? - views = views_txt.try &.as_s.gsub(/\D/, "").to_i64? + views_txt = extract_text( + video_primary_renderer + .try &.dig?("viewCount", "videoViewCountRenderer", "viewCount") + ) + views_txt ||= video_details["viewCount"]?.try &.as_s || "" + views = views_txt.gsub(/\D/, "").to_i64? length_txt = (microformat["lengthSeconds"]? || video_details["lengthSeconds"]) .try &.as_s.to_i64 From 3492485789ae3758f551916b406ed75b3c028021 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 21 Mar 2023 21:24:37 -0400 Subject: [PATCH 097/598] Fix channel search --- src/invidious/yt_backend/extractors.cr | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/invidious/yt_backend/extractors.cr b/src/invidious/yt_backend/extractors.cr index b14ad7b9..090944fc 100644 --- a/src/invidious/yt_backend/extractors.cr +++ b/src/invidious/yt_backend/extractors.cr @@ -18,6 +18,7 @@ private ITEM_PARSERS = { Parsers::CategoryRendererParser, Parsers::RichItemRendererParser, Parsers::ReelItemRendererParser, + Parsers::ItemSectionRendererParser, Parsers::ContinuationItemRendererParser, } @@ -377,6 +378,30 @@ private module Parsers end end + # Parses an InnerTube itemSectionRenderer into a SearchVideo. + # Returns nil when the given object isn't a ItemSectionRenderer + # + # A itemSectionRenderer seems to be a simple wrapper for a videoRenderer, used + # by the result page for channel searches. It is located inside a continuationItems + # container.It is very similar to RichItemRendererParser + # + module ItemSectionRendererParser + def self.process(item : JSON::Any, author_fallback : AuthorFallback) + if item_contents = item.dig?("itemSectionRenderer", "contents", 0) + return self.parse(item_contents, author_fallback) + end + end + + private def self.parse(item_contents, author_fallback) + child = VideoRendererParser.process(item_contents, author_fallback) + return child + end + + def self.parser_name + return {{@type.name}} + end + end + # Parses an InnerTube richItemRenderer into a SearchVideo. # Returns nil when the given object isn't a RichItemRenderer # From 5767344746fb9806e57cacb36ddc66ee2eaffd9e Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 21 Mar 2023 23:47:52 -0400 Subject: [PATCH 098/598] Fix parsing shorts on channel page --- src/invidious/channels/videos.cr | 31 ++----------- src/invidious/yt_backend/extractors.cr | 63 +++++++++++++------------- 2 files changed, 35 insertions(+), 59 deletions(-) diff --git a/src/invidious/channels/videos.cr b/src/invidious/channels/videos.cr index befec03d..fc2d1044 100644 --- a/src/invidious/channels/videos.cr +++ b/src/invidious/channels/videos.cr @@ -127,38 +127,15 @@ module Invidious::Channel::Tabs # Shorts # ------------------- - private def fetch_shorts_data(ucid : String, continuation : String? = nil) + def get_shorts(channel : AboutChannel, continuation : String? = nil) if continuation.nil? # EgZzaG9ydHPyBgUKA5oBAA%3D%3D is the protobuf object to load "shorts" # TODO: try to extract the continuation tokens that allows other sorting options - return YoutubeAPI.browse(ucid, params: "EgZzaG9ydHPyBgUKA5oBAA%3D%3D") + initial_data = YoutubeAPI.browse(channel.ucid, params: "EgZzaG9ydHPyBgUKA5oBAA%3D%3D") else - return YoutubeAPI.browse(continuation: continuation) - end - end - - def get_shorts(channel : AboutChannel, continuation : String? = nil) - initial_data = self.fetch_shorts_data(channel.ucid, continuation) - - begin - # Try to parse the initial data fetched above - return extract_items(initial_data, channel.author, channel.ucid) - rescue ex : RetryOnceException - # Sometimes, for a completely unknown reason, the "reelItemRenderer" - # object is missing some critical information (it happens once in about - # 20 subsequent requests). Refreshing the page is required to properly - # show the "shorts" tab. - # - # In order to make the experience smoother for the user, we simulate - # said page refresh by fetching again the JSON. If that still doesn't - # work, we raise a BrokenTubeException, as something is really broken. - begin - initial_data = self.fetch_shorts_data(channel.ucid, continuation) - return extract_items(initial_data, channel.author, channel.ucid) - rescue ex : RetryOnceException - raise BrokenTubeException.new "reelPlayerHeaderSupportedRenderers" - end + initial_data = YoutubeAPI.browse(continuation: continuation) end + return extract_items(initial_data, channel.author, channel.ucid) end # ------------------- diff --git a/src/invidious/yt_backend/extractors.cr b/src/invidious/yt_backend/extractors.cr index b14ad7b9..f952e767 100644 --- a/src/invidious/yt_backend/extractors.cr +++ b/src/invidious/yt_backend/extractors.cr @@ -423,42 +423,41 @@ private module Parsers "overlay", "reelPlayerOverlayRenderer" ) - # Sometimes, the "reelPlayerOverlayRenderer" object is missing the - # important part of the response. We use this exception to tell - # the calling function to fetch the content again. - if !reel_player_overlay.as_h.has_key?("reelPlayerHeaderSupportedRenderers") - raise RetryOnceException.new + if video_details_container = reel_player_overlay.dig?( + "reelPlayerHeaderSupportedRenderers", + "reelPlayerHeaderRenderer" + ) + # Author infos + + author = video_details_container + .dig?("channelTitleText", "runs", 0, "text") + .try &.as_s || author_fallback.name + + ucid = video_details_container + .dig?("channelNavigationEndpoint", "browseEndpoint", "browseId") + .try &.as_s || author_fallback.id + + # Title & publication date + + title = video_details_container.dig?("reelTitleText") + .try { |t| extract_text(t) } || "" + + published = video_details_container + .dig?("timestampText", "simpleText") + .try { |t| decode_date(t.as_s) } || Time.utc + + # View count + view_count_text = video_details_container.dig?("viewCountText", "simpleText") + else + author = author_fallback.name + ucid = author_fallback.id + published = Time.utc + title = item_contents.dig?("headline", "simpleText").try &.as_s || "" end - - video_details_container = reel_player_overlay.dig( - "reelPlayerHeaderSupportedRenderers", - "reelPlayerHeaderRenderer" - ) - - # Author infos - - author = video_details_container - .dig?("channelTitleText", "runs", 0, "text") - .try &.as_s || author_fallback.name - - ucid = video_details_container - .dig?("channelNavigationEndpoint", "browseEndpoint", "browseId") - .try &.as_s || author_fallback.id - - # Title & publication date - - title = video_details_container.dig?("reelTitleText") - .try { |t| extract_text(t) } || "" - - published = video_details_container - .dig?("timestampText", "simpleText") - .try { |t| decode_date(t.as_s) } || Time.utc - # View count # View count used to be in the reelWatchEndpoint, but that changed? - view_count_text = item_contents.dig?("viewCountText", "simpleText") - view_count_text ||= video_details_container.dig?("viewCountText", "simpleText") + view_count_text ||= item_contents.dig?("viewCountText", "simpleText") view_count = view_count_text.try &.as_s.gsub(/\D+/, "").to_i64? || 0_i64 From 49ddf8b6bdb98c7a9678cbec800c45350a54a786 Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Thu, 23 Mar 2023 05:10:21 +0000 Subject: [PATCH 099/598] Added attributed description support --- src/invidious/videos/description.cr | 81 +++++++++++++++++++++++++++++ src/invidious/videos/parser.cr | 6 ++- 2 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 src/invidious/videos/description.cr diff --git a/src/invidious/videos/description.cr b/src/invidious/videos/description.cr new file mode 100644 index 00000000..d4c60a84 --- /dev/null +++ b/src/invidious/videos/description.cr @@ -0,0 +1,81 @@ +require "json" +require "uri" + +def parse_command(command : JSON::Any?, string : String) : String? + on_tap = command.dig?("onTap", "innertubeCommand") + + # 3rd party URL, extract original URL from YouTube tracking URL + if url_endpoint = on_tap.try &.["urlEndpoint"]? + youtube_url = URI.parse url_endpoint["url"].as_s + + original_url = youtube_url.query_params["q"]? + if original_url.nil? + return "" + else + return "#{original_url}" + end + # 1st party watch URL + elsif watch_endpoint = on_tap.try &.["watchEndpoint"]? + video_id = watch_endpoint["videoId"].as_s + time = watch_endpoint["startTimeSeconds"].as_i + + url = "/watch?v=#{video_id}&t=#{time}s" + + # if text is a timestamp, use the string instead + if /(?:\d{2}:){1,2}\d{2}/ =~ string + return "#{string}" + else + return "#{url}" + end + # hashtag/other browse URLs + elsif browse_endpoint = on_tap.try &.dig?("commandMetadata", "webCommandMetadata") + url = browse_endpoint["url"].try &.as_s + + # remove unnecessary character in a channel name + if browse_endpoint["webPageType"]?.try &.as_s == "WEB_PAGE_TYPE_CHANNEL" + name = string.match(/@[\w\d]+/) + if name.try &.[0]? + return "#{name.try &.[0]}" + end + end + + return "#{string}" + end + + return "(unknown YouTube desc command)" +end + +def parse_description(desc : JSON::Any?) : String? + if desc.nil? + return "" + end + + content = desc["content"].as_s + if content.empty? + return "" + end + + if commands = desc["commandRuns"]?.try &.as_a + description = String.build do |str| + index = 0 + commands.each do |command| + start_index = command["startIndex"].as_i + length = command["length"].as_i + + if start_index > 0 && start_index - index > 0 + str << content[index..(start_index - 1)] + index += start_index - index + end + + str << parse_command(command, content[start_index, length]) + index += length + end + if index < content.size + str << content[index..content.size] + end + end + return description + end + + return content +end diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr index 608ae99d..3a342a95 100644 --- a/src/invidious/videos/parser.cr +++ b/src/invidious/videos/parser.cr @@ -284,8 +284,10 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any description = microformat.dig?("description", "simpleText").try &.as_s || "" short_description = player_response.dig?("videoDetails", "shortDescription") - description_html = video_secondary_renderer.try &.dig?("description", "runs") - .try &.as_a.try { |t| content_to_comment_html(t, video_id) } + # description_html = video_secondary_renderer.try &.dig?("description", "runs") + # .try &.as_a.try { |t| content_to_comment_html(t, video_id) } + + description_html = parse_description(video_secondary_renderer.try &.dig?("attributedDescription")) # Video metadata From 7755ed4ac8812377da04cff951324ab31d2e621c Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Thu, 23 Mar 2023 20:12:54 +0000 Subject: [PATCH 100/598] Fix regexs --- src/invidious/videos/description.cr | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/invidious/videos/description.cr b/src/invidious/videos/description.cr index d4c60a84..3d25197b 100644 --- a/src/invidious/videos/description.cr +++ b/src/invidious/videos/description.cr @@ -21,8 +21,9 @@ def parse_command(command : JSON::Any?, string : String) : String? url = "/watch?v=#{video_id}&t=#{time}s" - # if text is a timestamp, use the string instead - if /(?:\d{2}:){1,2}\d{2}/ =~ string + # if string is a timestamp, use the string instead + # this is a lazy regex for validating timestamps + if /(?:\d{1,2}:){1,2}\d{2}/ =~ string return "#{string}" else return "#{url}" @@ -33,7 +34,7 @@ def parse_command(command : JSON::Any?, string : String) : String? # remove unnecessary character in a channel name if browse_endpoint["webPageType"]?.try &.as_s == "WEB_PAGE_TYPE_CHANNEL" - name = string.match(/@[\w\d]+/) + name = string.match(/@[\w\d.-]+/) if name.try &.[0]? return "#{name.try &.[0]}" end From f840addd930945141a6d4fdf7e7eb8376411d82d Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Mon, 27 Mar 2023 22:10:28 -0400 Subject: [PATCH 101/598] Fix error when song title is missing from the track --- src/invidious/videos/parser.cr | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr index 608ae99d..13ee5f65 100644 --- a/src/invidious/videos/parser.cr +++ b/src/invidious/videos/parser.cr @@ -330,7 +330,10 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any # Used when the video has multiple songs if song_title = music_desc.dig?("carouselLockupRenderer", "videoLockup", "compactVideoRenderer", "title") # "simpleText" for plain text / "runs" when song has a link - song = song_title["simpleText"]? || song_title.dig("runs", 0, "text") + song = song_title["simpleText"]? || song_title.dig?("runs", 0, "text") + + # some videos can have empty tracks. See: https://www.youtube.com/watch?v=eBGIQ7ZuuiU + next if !song end music_desc.dig?("carouselLockupRenderer", "infoRows").try &.as_a.each do |desc| From a3da03bee91eab5c602882c4b43b959362ee441d Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Thu, 23 Mar 2023 18:10:53 -0400 Subject: [PATCH 102/598] improve accessibility --- assets/css/default.css | 29 ++++++++++++++----- assets/css/embed.css | 3 +- assets/js/_helpers.js | 8 +++-- assets/js/handlers.js | 2 +- assets/js/player.js | 2 +- src/invidious/comments.cr | 6 ++-- src/invidious/mixes.cr | 2 +- src/invidious/playlists.cr | 2 +- .../views/components/channel_info.ecr | 4 +-- src/invidious/views/components/item.ecr | 10 +++---- src/invidious/views/feeds/history.ecr | 2 +- src/invidious/views/watch.ecr | 4 +-- 12 files changed, 45 insertions(+), 29 deletions(-) diff --git a/assets/css/default.css b/assets/css/default.css index f8b1c9f7..65d03be1 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -119,13 +119,16 @@ body a.pure-button { button.pure-button-primary, body a.pure-button-primary, -.channel-owner:hover { +.channel-owner:hover, +.channel-owner:focus { background-color: #a0a0a0; color: rgba(35, 35, 35, 1); } button.pure-button-primary:hover, -body a.pure-button-primary:hover { +body a.pure-button-primary:hover, +button.pure-button-primary:focus, +body a.pure-button-primary:focus { background-color: rgba(0, 182, 240, 1); color: #fff; } @@ -227,6 +230,7 @@ div.watched-indicator { border-radius: 0; box-shadow: none; + appearance: none; -webkit-appearance: none; } @@ -365,11 +369,14 @@ span > select { .light-theme a:hover, .light-theme a:active, -.light-theme summary:hover { +.light-theme summary:hover, +.light-theme a:focus, +.light-theme summary:focus { color: #075A9E !important; } -.light-theme a.pure-button-primary:hover { +.light-theme a.pure-button-primary:hover, +.light-theme a.pure-button-primary:focus { color: #fff !important; } @@ -392,11 +399,14 @@ span > select { @media (prefers-color-scheme: light) { .no-theme a:hover, .no-theme a:active, - .no-theme summary:hover { + .no-theme summary:hover, + .no-theme a:focus, + .no-theme summary:focus { color: #075A9E !important; } - .no-theme a.pure-button-primary:hover { + .no-theme a.pure-button-primary:hover, + .no-theme a.pure-button-primary:focus { color: #fff !important; } @@ -423,7 +433,9 @@ span > select { .dark-theme a:hover, .dark-theme a:active, -.dark-theme summary:hover { +.dark-theme summary:hover, +.dark-theme a:focus, +.dark-theme summary:focus { color: rgb(0, 182, 240); } @@ -462,7 +474,8 @@ body.dark-theme { @media (prefers-color-scheme: dark) { .no-theme a:hover, - .no-theme a:active { + .no-theme a:active, + .no-theme a:focus { color: rgb(0, 182, 240); } diff --git a/assets/css/embed.css b/assets/css/embed.css index 466a284a..cbafcfea 100644 --- a/assets/css/embed.css +++ b/assets/css/embed.css @@ -21,6 +21,7 @@ color: white; } -.watch-on-invidious > a:hover { +.watch-on-invidious > a:hover, +.watch-on-invidious > a:focus { color: rgba(0, 182, 240, 1);; } diff --git a/assets/js/_helpers.js b/assets/js/_helpers.js index 7c50670e..3960cf2c 100644 --- a/assets/js/_helpers.js +++ b/assets/js/_helpers.js @@ -6,6 +6,7 @@ Array.prototype.find = Array.prototype.find || function (condition) { return this.filter(condition)[0]; }; + Array.from = Array.from || function (source) { return Array.prototype.slice.call(source); }; @@ -201,15 +202,16 @@ window.helpers = window.helpers || { if (localStorageIsUsable) { return { get: function (key) { - if (!localStorage[key]) return; + let storageItem = localStorage.getItem(key) + if (!storageItem) return; try { - return JSON.parse(decodeURIComponent(localStorage[key])); + return JSON.parse(decodeURIComponent(storageItem)); } catch(e) { // Erase non parsable value helpers.storage.remove(key); } }, - set: function (key, value) { localStorage[key] = encodeURIComponent(JSON.stringify(value)); }, + set: function (key, value) { localStorage.setItem(key, encodeURIComponent(JSON.stringify(value))); }, remove: function (key) { localStorage.removeItem(key); } }; } diff --git a/assets/js/handlers.js b/assets/js/handlers.js index 29810e72..539974fb 100644 --- a/assets/js/handlers.js +++ b/assets/js/handlers.js @@ -137,7 +137,7 @@ if (focused_tag === 'textarea') return; if (focused_tag === 'input') { let focused_type = document.activeElement.type.toLowerCase(); - if (!focused_type.match(allowed)) return; + if (!allowed.test(focused_type)) return; } // Focus search bar on '/' diff --git a/assets/js/player.js b/assets/js/player.js index ee678663..bb53ac24 100644 --- a/assets/js/player.js +++ b/assets/js/player.js @@ -261,7 +261,7 @@ function updateCookie(newVolume, newSpeed) { var date = new Date(); date.setFullYear(date.getFullYear() + 2); - var ipRegex = /^((\d+\.){3}\d+|[A-Fa-f0-9]*:[A-Fa-f0-9:]*:[A-Fa-f0-9:]+)$/; + var ipRegex = /^((\d+\.){3}\d+|[\dA-Fa-f]*:[\d:A-Fa-f]*:[\d:A-Fa-f]+)$/; var domainUsed = location.hostname; // Fix for a bug in FF where the leading dot in the FQDN is not ignored diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr index b15d63d4..2d62580d 100644 --- a/src/invidious/comments.cr +++ b/src/invidious/comments.cr @@ -346,7 +346,7 @@ def template_youtube_comments(comments, locale, thin_mode, is_replies = false) html << <<-END_HTML
- +

@@ -367,7 +367,7 @@ def template_youtube_comments(comments, locale, thin_mode, is_replies = false) html << <<-END_HTML

- +
END_HTML @@ -428,7 +428,7 @@ def template_youtube_comments(comments, locale, thin_mode, is_replies = false) html << <<-END_HTML
- +
diff --git a/src/invidious/mixes.cr b/src/invidious/mixes.cr index 3f342b92..defbbc84 100644 --- a/src/invidious/mixes.cr +++ b/src/invidious/mixes.cr @@ -97,7 +97,7 @@ def template_mix(mix)
  • - +

    #{recode_length_seconds(video["lengthSeconds"].as_i)}

    #{video["title"]}

    diff --git a/src/invidious/playlists.cr b/src/invidious/playlists.cr index 57f1f53e..40bb244b 100644 --- a/src/invidious/playlists.cr +++ b/src/invidious/playlists.cr @@ -507,7 +507,7 @@ def template_playlist(playlist)
  • - +

    #{recode_length_seconds(video["lengthSeconds"].as_i)}

    #{video["title"]}

    diff --git a/src/invidious/views/components/channel_info.ecr b/src/invidious/views/components/channel_info.ecr index f216359f..d94ecdad 100644 --- a/src/invidious/views/components/channel_info.ecr +++ b/src/invidious/views/components/channel_info.ecr @@ -1,6 +1,6 @@ <% if channel.banner %>
    - "> + " alt="">
    @@ -11,7 +11,7 @@
    - + <%= author %><% if !channel.verified.nil? && channel.verified %> <% end %>
    diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr index fa12374f..36e9d45b 100644 --- a/src/invidious/views/components/item.ecr +++ b/src/invidious/views/components/item.ecr @@ -7,7 +7,7 @@
    <% if !env.get("preferences").as(Preferences).thin_mode %>
    - "/> + " alt=""/>
    <% end %>

    <%= HTML.escape(item.author) %><% if !item.author_verified.nil? && item.author_verified %> <% end %>

    @@ -25,7 +25,7 @@
    <% if !env.get("preferences").as(Preferences).thin_mode %>
    - "/> + " alt="" />

    <%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %>

    <% end %> @@ -38,7 +38,7 @@
    <% if !env.get("preferences").as(Preferences).thin_mode %>
    - + <% if item.length_seconds != 0 %>

    <%= recode_length_seconds(item.length_seconds) %>

    <% end %> @@ -58,7 +58,7 @@
    <% if !env.get("preferences").as(Preferences).thin_mode %>
    - + <% if plid_form = env.get?("remove_playlist_items") %> " method="post"> @@ -112,7 +112,7 @@ <% if !env.get("preferences").as(Preferences).thin_mode %>
    - + <% if env.get? "show_watched" %> " method="post"> "> diff --git a/src/invidious/views/feeds/history.ecr b/src/invidious/views/feeds/history.ecr index 471d21db..be1b521d 100644 --- a/src/invidious/views/feeds/history.ecr +++ b/src/invidious/views/feeds/history.ecr @@ -34,7 +34,7 @@ <% if !env.get("preferences").as(Preferences).thin_mode %>
    - + " method="post"> ">

    diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index a3ec94e8..d2082557 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -208,7 +208,7 @@ we're going to need to do it here in order to allow for translations.

    @@ -298,7 +298,7 @@ we're going to need to do it here in order to allow for translations. &listen=<%= params.listen %>"> <% if !env.get("preferences").as(Preferences).thin_mode %>
    - /mqdefault.jpg"> + /mqdefault.jpg" alt="">

    <%= recode_length_seconds(rv["length_seconds"]?.try &.to_i? || 0) %>

    <% end %> From 1d187bcf176481c5619e275e60ac70a1bee80269 Mon Sep 17 00:00:00 2001 From: Lennart Bernhardt Date: Tue, 28 Mar 2023 10:30:52 +0200 Subject: [PATCH 103/598] fix long description overflow --- assets/css/default.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/assets/css/default.css b/assets/css/default.css index f8b1c9f7..fcc826d1 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -515,6 +515,10 @@ hr { #descexpansionbutton ~ div { overflow: hidden; + height: 8.3em; +} + +#descexpansionbutton:not(:checked) ~ div { max-height: 8.3em; } From f83f0d2561494eff915f32b4c9364e87000f60d5 Mon Sep 17 00:00:00 2001 From: Lennart Bernhardt Date: Tue, 28 Mar 2023 10:33:03 +0200 Subject: [PATCH 104/598] remove fixed height from description --- assets/css/default.css | 1 - 1 file changed, 1 deletion(-) diff --git a/assets/css/default.css b/assets/css/default.css index fcc826d1..88ec6ef1 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -515,7 +515,6 @@ hr { #descexpansionbutton ~ div { overflow: hidden; - height: 8.3em; } #descexpansionbutton:not(:checked) ~ div { From 73d2ed6f77308dd300e68f3ea059c6aa2c10b1ce Mon Sep 17 00:00:00 2001 From: techmetx11 Date: Wed, 29 Mar 2023 23:33:23 +0000 Subject: [PATCH 105/598] Optimize some redundant stuff --- src/invidious/videos/description.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/invidious/videos/description.cr b/src/invidious/videos/description.cr index 3d25197b..b1d851d3 100644 --- a/src/invidious/videos/description.cr +++ b/src/invidious/videos/description.cr @@ -64,8 +64,8 @@ def parse_description(desc : JSON::Any?) : String? length = command["length"].as_i if start_index > 0 && start_index - index > 0 - str << content[index..(start_index - 1)] - index += start_index - index + str << content[index...start_index] + index = start_index end str << parse_command(command, content[start_index, length]) From 0fe1b1ec19d8bf108765842dc84252fc3b394a9b Mon Sep 17 00:00:00 2001 From: Jarek Baran Date: Thu, 30 Mar 2023 12:52:03 +0200 Subject: [PATCH 106/598] download_widget: Add missing translation key --- locales/en-US.json | 1 + locales/pl.json | 1 + src/invidious/frontend/watch_page.cr | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/locales/en-US.json b/locales/en-US.json index a3c195ff..05811f27 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -402,6 +402,7 @@ "Movies": "Movies", "Download": "Download", "Download as: ": "Download as: ", + "Download is disabled": "Download is disabled", "%A %B %-d, %Y": "%A %B %-d, %Y", "(edited)": "(edited)", "YouTube comment permalink": "YouTube comment permalink", diff --git a/locales/pl.json b/locales/pl.json index 3ca78e43..3c713e70 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -317,6 +317,7 @@ "Movies": "Filmy", "Download": "Pobierz", "Download as: ": "Pobierz jako: ", + "Download is disabled": "Pobieranie jest wyłączone", "%A %B %-d, %Y": "%A, %-d %B %Y", "(edited)": "(edytowany)", "YouTube comment permalink": "Odnośnik bezpośredni do komentarza na YouTube", diff --git a/src/invidious/frontend/watch_page.cr b/src/invidious/frontend/watch_page.cr index a9b00860..e3214469 100644 --- a/src/invidious/frontend/watch_page.cr +++ b/src/invidious/frontend/watch_page.cr @@ -20,7 +20,7 @@ module Invidious::Frontend::WatchPage def download_widget(locale : String, video : Video, video_assets : VideoAssets) : String if CONFIG.disabled?("downloads") - return "

    #{translate(locale, "Download is disabled.")}

    " + return "

    #{translate(locale, "Download is disabled")}

    " end return String.build(4000) do |str| From e0600f455393ffcf0edd2f0c4b644fac7dba209f Mon Sep 17 00:00:00 2001 From: Emilien Devos Date: Fri, 31 Mar 2023 22:08:09 +0200 Subject: [PATCH 107/598] quick fix for channel videos page --- src/invidious/channels/videos.cr | 4 +++- src/invidious/yt_backend/extractors.cr | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/invidious/channels/videos.cr b/src/invidious/channels/videos.cr index befec03d..3d53f2ab 100644 --- a/src/invidious/channels/videos.cr +++ b/src/invidious/channels/videos.cr @@ -30,7 +30,9 @@ def produce_channel_videos_continuation(ucid, page = 1, auto_generated = nil, so "15:embedded" => { "1:embedded" => { "1:string" => object_inner_2_encoded, - "2:string" => "00000000-0000-0000-0000-000000000000", + }, + "2:embedded" => { + "1:string" => "00000000-0000-0000-0000-000000000000", }, "3:varint" => sort_by_numerical, }, diff --git a/src/invidious/yt_backend/extractors.cr b/src/invidious/yt_backend/extractors.cr index b14ad7b9..978e380d 100644 --- a/src/invidious/yt_backend/extractors.cr +++ b/src/invidious/yt_backend/extractors.cr @@ -773,6 +773,7 @@ end def extract_items(initial_data : InitialData, &block) if unpackaged_data = initial_data["contents"]?.try &.as_h elsif unpackaged_data = initial_data["response"]?.try &.as_h + elsif unpackaged_data = initial_data.dig?("onResponseReceivedActions", 1).try &.as_h elsif unpackaged_data = initial_data.dig?("onResponseReceivedActions", 0).try &.as_h else unpackaged_data = initial_data From 1da00bade3d370711c670afb38dcd0f97e9dd965 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Sun, 2 Apr 2023 16:31:59 -0400 Subject: [PATCH 108/598] implement code suggestions Co-Authored-By: Samantaz Fox --- assets/js/_helpers.js | 5 ++++- src/invidious/comments.cr | 8 ++++---- src/invidious/mixes.cr | 2 +- src/invidious/playlists.cr | 2 +- src/invidious/views/components/channel_info.ecr | 4 ++-- src/invidious/views/components/item.ecr | 8 ++++---- src/invidious/views/feeds/history.ecr | 2 +- src/invidious/views/watch.ecr | 4 ++-- 8 files changed, 19 insertions(+), 16 deletions(-) diff --git a/assets/js/_helpers.js b/assets/js/_helpers.js index 3960cf2c..8e18169e 100644 --- a/assets/js/_helpers.js +++ b/assets/js/_helpers.js @@ -211,7 +211,10 @@ window.helpers = window.helpers || { helpers.storage.remove(key); } }, - set: function (key, value) { localStorage.setItem(key, encodeURIComponent(JSON.stringify(value))); }, + set: function (key, value) { + let encoded_value = encodeURIComponent(JSON.stringify(value)) + localStorage.setItem(key, encoded_value); + }, remove: function (key) { localStorage.removeItem(key); } }; } diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr index 2d62580d..fd2be73d 100644 --- a/src/invidious/comments.cr +++ b/src/invidious/comments.cr @@ -346,7 +346,7 @@ def template_youtube_comments(comments, locale, thin_mode, is_replies = false) html << <<-END_HTML
    - +

    @@ -367,7 +367,7 @@ def template_youtube_comments(comments, locale, thin_mode, is_replies = false) html << <<-END_HTML

    - +
    END_HTML @@ -428,7 +428,7 @@ def template_youtube_comments(comments, locale, thin_mode, is_replies = false) html << <<-END_HTML
    - +
    @@ -702,7 +702,7 @@ def content_to_comment_html(content, video_id : String? = "") str << %(title=") << emojiAlt << "\" " str << %(width=") << emojiThumb["width"] << "\" " str << %(height=") << emojiThumb["height"] << "\" " - str << %(class="channel-emoji"/>) + str << %(class="channel-emoji" />) end else # Hide deleted channel emoji diff --git a/src/invidious/mixes.cr b/src/invidious/mixes.cr index defbbc84..823ca85b 100644 --- a/src/invidious/mixes.cr +++ b/src/invidious/mixes.cr @@ -97,7 +97,7 @@ def template_mix(mix)
  • - +

    #{recode_length_seconds(video["lengthSeconds"].as_i)}

    #{video["title"]}

    diff --git a/src/invidious/playlists.cr b/src/invidious/playlists.cr index 40bb244b..013be268 100644 --- a/src/invidious/playlists.cr +++ b/src/invidious/playlists.cr @@ -507,7 +507,7 @@ def template_playlist(playlist)
  • - +

    #{recode_length_seconds(video["lengthSeconds"].as_i)}

    #{video["title"]}

    diff --git a/src/invidious/views/components/channel_info.ecr b/src/invidious/views/components/channel_info.ecr index d94ecdad..59888760 100644 --- a/src/invidious/views/components/channel_info.ecr +++ b/src/invidious/views/components/channel_info.ecr @@ -1,6 +1,6 @@ <% if channel.banner %>
    - " alt=""> + " alt="" />
    @@ -11,7 +11,7 @@
    - + <%= author %><% if !channel.verified.nil? && channel.verified %> <% end %>
    diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr index 36e9d45b..7cfd38db 100644 --- a/src/invidious/views/components/item.ecr +++ b/src/invidious/views/components/item.ecr @@ -7,7 +7,7 @@
    <% if !env.get("preferences").as(Preferences).thin_mode %>
    - " alt=""/> + " alt="" />
    <% end %>

    <%= HTML.escape(item.author) %><% if !item.author_verified.nil? && item.author_verified %> <% end %>

    @@ -38,7 +38,7 @@
    <% if !env.get("preferences").as(Preferences).thin_mode %>
    - + <% if item.length_seconds != 0 %>

    <%= recode_length_seconds(item.length_seconds) %>

    <% end %> @@ -58,7 +58,7 @@
    <% if !env.get("preferences").as(Preferences).thin_mode %>
    - + <% if plid_form = env.get?("remove_playlist_items") %> " method="post"> @@ -112,7 +112,7 @@ <% if !env.get("preferences").as(Preferences).thin_mode %>
    - + <% if env.get? "show_watched" %> " method="post"> "> diff --git a/src/invidious/views/feeds/history.ecr b/src/invidious/views/feeds/history.ecr index be1b521d..2234b297 100644 --- a/src/invidious/views/feeds/history.ecr +++ b/src/invidious/views/feeds/history.ecr @@ -34,7 +34,7 @@ <% if !env.get("preferences").as(Preferences).thin_mode %>
    - + " method="post"> ">

    diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index d2082557..5b3190f3 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -208,7 +208,7 @@ we're going to need to do it here in order to allow for translations.

    @@ -298,7 +298,7 @@ we're going to need to do it here in order to allow for translations. &listen=<%= params.listen %>"> <% if !env.get("preferences").as(Preferences).thin_mode %>
    - /mqdefault.jpg" alt=""> + /mqdefault.jpg" alt="" />

    <%= recode_length_seconds(rv["length_seconds"]?.try &.to_i? || 0) %>

    <% end %> From e3c1cb3ec9d40b587435020a9e53ec477e69a7ae Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Sun, 2 Apr 2023 16:45:34 -0400 Subject: [PATCH 109/598] fix view count extraction --- src/invidious/yt_backend/extractors.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/yt_backend/extractors.cr b/src/invidious/yt_backend/extractors.cr index 347d2482..9c041361 100644 --- a/src/invidious/yt_backend/extractors.cr +++ b/src/invidious/yt_backend/extractors.cr @@ -484,7 +484,7 @@ private module Parsers # View count used to be in the reelWatchEndpoint, but that changed? view_count_text ||= item_contents.dig?("viewCountText", "simpleText") - view_count = view_count_text.try &.as_s.gsub(/\D+/, "").to_i64? || 0_i64 + view_count = short_text_to_number(view_count_text.try &.as_s || "0") # Duration From 600da635b78f3cabee327361866f1ff0c78c0438 Mon Sep 17 00:00:00 2001 From: raphj Date: Sun, 2 Apr 2023 23:36:06 +0200 Subject: [PATCH 110/598] Allow browser suggestions for search (#3704) --- src/invidious/views/components/search_box.ecr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/views/components/search_box.ecr b/src/invidious/views/components/search_box.ecr index 1240e5bd..a03785d1 100644 --- a/src/invidious/views/components/search_box.ecr +++ b/src/invidious/views/components/search_box.ecr @@ -1,6 +1,6 @@
    - autofocus<% end %> name="q" placeholder="<%= translate(locale, "search") %>" title="<%= translate(locale, "search") %>" From fffdaa1410db7f9b5c67b1b47d401a2744e7b220 Mon Sep 17 00:00:00 2001 From: thtmnisamnstr Date: Mon, 3 Apr 2023 17:07:58 -0700 Subject: [PATCH 111/598] 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 b3c0afef02ee13c7f291fd26a5d64b4aee059906 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Wed, 5 Apr 2023 23:43:41 +0200 Subject: [PATCH 112/598] Videos: fix description text offset when emojis are present --- src/invidious/videos/description.cr | 71 +++++++++++++++++++---------- 1 file changed, 47 insertions(+), 24 deletions(-) diff --git a/src/invidious/videos/description.cr b/src/invidious/videos/description.cr index b1d851d3..2017955d 100644 --- a/src/invidious/videos/description.cr +++ b/src/invidious/videos/description.cr @@ -46,37 +46,60 @@ def parse_command(command : JSON::Any?, string : String) : String? return "(unknown YouTube desc command)" end -def parse_description(desc : JSON::Any?) : String? - if desc.nil? - return "" +private def copy_string(str : String::Builder, iter : Iterator, count : Int) : Int + copied = 0 + while copied < count + cp = iter.next + break if cp.is_a?(Iterator::Stop) + + str << cp.chr + + # A codepoint from the SMP counts twice + copied += 1 if cp > 0xFFFF + copied += 1 end + return copied +end + +def parse_description(desc : JSON::Any?) : String? + return "" if desc.nil? + content = desc["content"].as_s - if content.empty? - return "" - end + return "" if content.empty? - if commands = desc["commandRuns"]?.try &.as_a - description = String.build do |str| - index = 0 - commands.each do |command| - start_index = command["startIndex"].as_i - length = command["length"].as_i + commands = desc["commandRuns"]?.try &.as_a + return content if commands.nil? - if start_index > 0 && start_index - index > 0 - str << content[index...start_index] - index = start_index - end + # Not everything is stored in UTF-8 on youtube's side. The SMP codepoints + # (0x10000 and above) are encoded as UTF-16 surrogate pairs, which are + # automatically decoded by the JSON parser. It means that we need to count + # copied byte in a special manner, preventing the use of regular string copy. + iter = content.each_codepoint - str << parse_command(command, content[start_index, length]) - index += length - end - if index < content.size - str << content[index..content.size] + index = 0 + + return String.build do |str| + commands.each do |command| + cmd_start = command["startIndex"].as_i + cmd_length = command["length"].as_i + + # Copy the text chunk between this command and the previous if needed. + length = cmd_start - index + index += copy_string(str, iter, length) + + # We need to copy the command's text using the iterator + # and the special function defined above. + cmd_content = String.build(cmd_length) do |str2| + copy_string(str2, iter, cmd_length) end + + str << parse_command(command, cmd_content) + index += cmd_length end - return description - end - return content + # Copy the end of the string (past the last command). + remaining_length = content.size - index + copy_string(str, iter, remaining_length) if remaining_length > 0 + end end From 9a765418d1410ceda3a27ebcd2febd9fe4319edc Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Mon, 10 Apr 2023 16:59:13 +0200 Subject: [PATCH 113/598] Update specs --- mocks | 2 +- .../videos/regular_videos_extract_spec.cr | 34 +++++++++---------- .../videos/scheduled_live_extract_spec.cr | 7 ++-- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/mocks b/mocks index cb16e034..11ec372f 160000 --- a/mocks +++ b/mocks @@ -1 +1 @@ -Subproject commit cb16e0343c8f94182615610bfe3c503db89717a7 +Subproject commit 11ec372f72747c09d48ffef04843f72be67d5b54 diff --git a/spec/invidious/videos/regular_videos_extract_spec.cr b/spec/invidious/videos/regular_videos_extract_spec.cr index cbe80010..a6a3e60a 100644 --- a/spec/invidious/videos/regular_videos_extract_spec.cr +++ b/spec/invidious/videos/regular_videos_extract_spec.cr @@ -17,8 +17,8 @@ Spectator.describe "parse_video_info" do # Basic video infos expect(info["title"].as_s).to eq("I Gave My 100,000,000th Subscriber An Island") - expect(info["views"].as_i).to eq(115_784_415) - expect(info["likes"].as_i).to eq(4_932_790) + expect(info["views"].as_i).to eq(126_573_823) + expect(info["likes"].as_i).to eq(5_157_654) # For some reason the video length from VideoDetails and the # one from microformat differs by 1s... @@ -48,12 +48,12 @@ Spectator.describe "parse_video_info" do expect(info["relatedVideos"].as_a.size).to eq(20) - expect(info["relatedVideos"][0]["id"]).to eq("iogcY_4xGjo") - expect(info["relatedVideos"][0]["title"]).to eq("$1 vs $1,000,000 Hotel Room!") + expect(info["relatedVideos"][0]["id"]).to eq("Hwybp38GnZw") + expect(info["relatedVideos"][0]["title"]).to eq("I Built Willy Wonka's Chocolate Factory!") expect(info["relatedVideos"][0]["author"]).to eq("MrBeast") expect(info["relatedVideos"][0]["ucid"]).to eq("UCX6OQ3DkcsbYNE6H8uQQuVA") - expect(info["relatedVideos"][0]["view_count"]).to eq("172972109") - expect(info["relatedVideos"][0]["short_view_count"]).to eq("172M") + expect(info["relatedVideos"][0]["view_count"]).to eq("179877630") + expect(info["relatedVideos"][0]["short_view_count"]).to eq("179M") expect(info["relatedVideos"][0]["author_verified"]).to eq("true") # Description @@ -76,11 +76,11 @@ Spectator.describe "parse_video_info" do expect(info["ucid"].as_s).to eq("UCX6OQ3DkcsbYNE6H8uQQuVA") expect(info["authorThumbnail"].as_s).to eq( - "https://yt3.ggpht.com/ytc/AL5GRJUfhQdJS6n-YJtsAf-ouS2myDavDOq_zXBfebal3Q=s48-c-k-c0x00ffffff-no-rj" + "https://yt3.ggpht.com/ytc/AL5GRJVuqw82ERvHzsmBxL7avr1dpBtsVIXcEzBPZaloFg=s48-c-k-c0x00ffffff-no-rj" ) expect(info["authorVerified"].as_bool).to be_true - expect(info["subCountText"].as_s).to eq("135M") + expect(info["subCountText"].as_s).to eq("143M") end it "parses a regular video with no descrition/comments" do @@ -99,7 +99,7 @@ Spectator.describe "parse_video_info" do # Basic video infos expect(info["title"].as_s).to eq("Chris Rea - Auberge") - expect(info["views"].as_i).to eq(10_698_554) + expect(info["views"].as_i).to eq(10_943_126) expect(info["likes"].as_i).to eq(0) expect(info["lengthSeconds"].as_i).to eq(283_i64) expect(info["published"].as_s).to eq("2012-05-21T00:00:00Z") @@ -132,21 +132,21 @@ Spectator.describe "parse_video_info" do # Related videos - expect(info["relatedVideos"].as_a.size).to eq(18) + expect(info["relatedVideos"].as_a.size).to eq(19) - expect(info["relatedVideos"][0]["id"]).to eq("rfyZrJUmzxU") - expect(info["relatedVideos"][0]["title"]).to eq("cheb mami - bekatni") - expect(info["relatedVideos"][0]["author"]).to eq("pelitovic") - expect(info["relatedVideos"][0]["ucid"]).to eq("UCsp6vFyJeGoLxgn-AsHp1tw") - expect(info["relatedVideos"][0]["view_count"]).to eq("13863619") - expect(info["relatedVideos"][0]["short_view_count"]).to eq("13M") + expect(info["relatedVideos"][0]["id"]).to eq("Ww3KeZ2_Yv4") + expect(info["relatedVideos"][0]["title"]).to eq("Chris Rea") + expect(info["relatedVideos"][0]["author"]).to eq("PanMusic") + expect(info["relatedVideos"][0]["ucid"]).to eq("UCsKAPSuh1iNbLWUga_igPyA") + expect(info["relatedVideos"][0]["view_count"]).to eq("31581") + expect(info["relatedVideos"][0]["short_view_count"]).to eq("31K") expect(info["relatedVideos"][0]["author_verified"]).to eq("false") # Description expect(info["description"].as_s).to eq(" ") expect(info["shortDescription"].as_s).to be_empty - expect(info["descriptionHtml"].as_s).to eq("

    ") + expect(info["descriptionHtml"].as_s).to eq("") # Video metadata diff --git a/spec/invidious/videos/scheduled_live_extract_spec.cr b/spec/invidious/videos/scheduled_live_extract_spec.cr index 9dd22b97..25e08c51 100644 --- a/spec/invidious/videos/scheduled_live_extract_spec.cr +++ b/spec/invidious/videos/scheduled_live_extract_spec.cr @@ -86,9 +86,10 @@ Spectator.describe "parse_video_info" do expect(info["description"].as_s).to start_with(description_start_text) expect(info["shortDescription"].as_s).to start_with(description_start_text) - expect(info["descriptionHtml"].as_s).to start_with( - "PBD Podcast Episode 241. The home team is ready and at it again with the latest news, interesting topics and trending conversations on topics that matter. Try our sponsor Aura for 14 days free -
    aura.com/pbd" - ) + # TODO: Update mocks right before the start of PDB podcast, either on friday or saturday (time unknown) + # expect(info["descriptionHtml"].as_s).to start_with( + # "PBD Podcast Episode 241. The home team is ready and at it again with the latest news, interesting topics and trending conversations on topics that matter. Try our sponsor Aura for 14 days free - aura.com/pbd" + # ) # Video metadata From 5517a4eadb980ae06d4dde08afd10fec8c83f9b4 Mon Sep 17 00:00:00 2001 From: chunky programmer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Wed, 19 Apr 2023 23:29:50 -0400 Subject: [PATCH 114/598] fix fetching community continuations --- src/invidious/channels/community.cr | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/invidious/channels/community.cr b/src/invidious/channels/community.cr index ce34ff82..ad786f3a 100644 --- a/src/invidious/channels/community.cr +++ b/src/invidious/channels/community.cr @@ -31,18 +31,16 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode) session_token: session_token, } - response = YT_POOL.client &.post("/comment_service_ajax?action_get_comments=1&ctoken=#{continuation}&continuation=#{continuation}&hl=en&gl=US", headers, form: post_req) - body = JSON.parse(response.body) + body = YoutubeAPI.browse(continuation) - body = body["response"]["continuationContents"]["itemSectionContinuation"]? || - body["response"]["continuationContents"]["backstageCommentsContinuation"]? + body = body.dig?("continuationContents", "itemSectionContinuation") || + body.dig?("continuationContents", "backstageCommentsContinuation") if !body raise InfoException.new("Could not extract continuation.") end end - continuation = body["continuations"]?.try &.[0]["nextContinuationData"]["continuation"].as_s posts = body["contents"].as_a if message = posts[0]["messageRenderer"]? @@ -270,10 +268,8 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode) end end end - - if body["continuations"]? - continuation = body["continuations"][0]["nextContinuationData"]["continuation"].as_s - json.field "continuation", extract_channel_community_cursor(continuation) + if cont = posts.dig?(-1, "continuationItemRenderer", "continuationEndpoint", "continuationCommand", "token") + json.field "continuation", extract_channel_community_cursor(cont.as_s) end end end From d1b51e57a2aa2fd29cf0d1ebed71dcce7ba4ac1c Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Tue, 25 Apr 2023 22:51:21 +0200 Subject: [PATCH 115/598] CI: Add crystal 1.7.3 and 1.8.1 --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4aa334c9..565cf8cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,6 +42,8 @@ jobs: - 1.4.1 - 1.5.1 - 1.6.2 + - 1.7.3 + - 1.8.1 include: - crystal: nightly stable: false From e24feab1f7eddbb912b2ea874800c061eebd8dcc Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Tue, 25 Apr 2023 22:51:56 +0200 Subject: [PATCH 116/598] CI: Remove crystal 1.3.2 --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 565cf8cd..96903fc7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,7 +38,6 @@ jobs: matrix: stable: [true] crystal: - - 1.3.2 - 1.4.1 - 1.5.1 - 1.6.2 From 0107b774f29b0f4cc0a7fabe546db347390337ec Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Fri, 28 Apr 2023 17:23:40 +0200 Subject: [PATCH 117/598] Trending: Don't extract items from categories --- src/invidious/trending.cr | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/invidious/trending.cr b/src/invidious/trending.cr index 134eb437..74bab1bd 100644 --- a/src/invidious/trending.cr +++ b/src/invidious/trending.cr @@ -17,7 +17,9 @@ def fetch_trending(trending_type, region, locale) client_config = YoutubeAPI::ClientConfig.new(region: region) initial_data = YoutubeAPI.browse("FEtrending", params: params, client_config: client_config) - trending = extract_videos(initial_data) - return {trending, plid} + items, _ = extract_items(initial_data) + + # Return items, but ignore categories (e.g featured content) + return items.reject!(Category), plid end From 7afa03d821365673e955468eff58009b5fb5c4c8 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Fri, 28 Apr 2023 17:27:06 +0200 Subject: [PATCH 118/598] Search: Don't extract items from categories too --- src/invidious/search/processors.cr | 4 ++-- src/invidious/search/query.cr | 23 +---------------------- 2 files changed, 3 insertions(+), 24 deletions(-) diff --git a/src/invidious/search/processors.cr b/src/invidious/search/processors.cr index 7e909590..25edb936 100644 --- a/src/invidious/search/processors.cr +++ b/src/invidious/search/processors.cr @@ -10,7 +10,7 @@ module Invidious::Search initial_data = YoutubeAPI.search(query.text, search_params, client_config: client_config) items, _ = extract_items(initial_data) - return items + return items.reject!(Category) end # Search a youtube channel @@ -32,7 +32,7 @@ module Invidious::Search response_json = YoutubeAPI.browse(continuation) items, _ = extract_items(response_json, "", ucid) - return items + return items.reject!(Category) end # Search inside of user subscriptions diff --git a/src/invidious/search/query.cr b/src/invidious/search/query.cr index 24e79609..e38845d9 100644 --- a/src/invidious/search/query.cr +++ b/src/invidious/search/query.cr @@ -113,7 +113,7 @@ module Invidious::Search case @type when .regular?, .playlist? - items = unnest_items(Processors.regular(self)) + items = Processors.regular(self) # when .channel? items = Processors.channel(self) @@ -136,26 +136,5 @@ module Invidious::Search return params end - - # TODO: clean code - private def unnest_items(all_items) : Array(SearchItem) - items = [] of SearchItem - - # Light processing to flatten search results out of Categories. - # They should ideally be supported in the future. - all_items.each do |i| - if i.is_a? Category - i.contents.each do |nest_i| - if !nest_i.is_a? Video - items << nest_i - end - end - else - items << i - end - end - - return items - end end end From 3cfbc19ccc031ec4640f5e06568d2a52ebf90627 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Fri, 28 Apr 2023 17:30:01 +0200 Subject: [PATCH 119/598] Extractors: Add utility function to extract items from categories --- src/invidious/yt_backend/extractors_utils.cr | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/invidious/yt_backend/extractors_utils.cr b/src/invidious/yt_backend/extractors_utils.cr index 0cb3c079..b247dca8 100644 --- a/src/invidious/yt_backend/extractors_utils.cr +++ b/src/invidious/yt_backend/extractors_utils.cr @@ -68,19 +68,16 @@ rescue ex return false end -def extract_videos(initial_data : Hash(String, JSON::Any), author_fallback : String? = nil, author_id_fallback : String? = nil) : Array(SearchVideo) - extracted, _ = extract_items(initial_data, author_fallback, author_id_fallback) +# This function extracts the SearchItems from a Category. +# Categories are commonly returned in search results and trending pages. +def extract_category(category : Category) : Array(SearchVideo) + items = [] of SearchItem - target = [] of (SearchItem | Continuation) - extracted.each do |i| - if i.is_a?(Category) - i.contents.each { |cate_i| target << cate_i if !cate_i.is_a? Video } - else - target << i - end + category.contents.each do |item| + target << cate_i if item.is_a?(SearchItem) end - return target.select(SearchVideo) + return items end def extract_selected_tab(tabs) From 67859113fdaac70f4524c44fd24913824889b691 Mon Sep 17 00:00:00 2001 From: AHOHNMYC Date: Wed, 22 Mar 2023 06:57:05 +0000 Subject: [PATCH 120/598] Update Russian translation --- locales/ru.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/locales/ru.json b/locales/ru.json index 7ca5cf1f..d2d7c86d 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -495,5 +495,8 @@ "channel_tab_shorts_label": "Shorts", "Music in this video": "Музыка в этом видео", "Artist: ": "Исполнитель: ", - "Album: ": "Альбом: " + "Album: ": "Альбом: ", + "Song: ": "Композиция: ", + "Standard YouTube license": "Стандартная лицензия YouTube", + "Channel Sponsor": "Спонсор канала" } From 17ecdbaf7db2843e9b8d0977228cc5c8c18ecf39 Mon Sep 17 00:00:00 2001 From: Ashirg-ch Date: Wed, 22 Mar 2023 09:19:19 +0000 Subject: [PATCH 121/598] Update German translation --- locales/de.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/locales/de.json b/locales/de.json index c2941d6d..0df86663 100644 --- a/locales/de.json +++ b/locales/de.json @@ -479,5 +479,8 @@ "Artist: ": "Künstler: ", "Album: ": "Album: ", "channel_tab_playlists_label": "Wiedergabelisten", - "channel_tab_channels_label": "Kanäle" + "channel_tab_channels_label": "Kanäle", + "Channel Sponsor": "Kanalsponsor", + "Standard YouTube license": "Standard YouTube-Lizenz", + "Song: ": "Musik: " } From 155f5fef97c75ad242f623c0b97b1422ee832ec5 Mon Sep 17 00:00:00 2001 From: Matthaiks Date: Mon, 20 Mar 2023 19:10:20 +0000 Subject: [PATCH 122/598] 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 3c713e70..2b6768d9 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -498,5 +498,6 @@ "Artist: ": "Wykonawca: ", "Album: ": "Album: ", "Song: ": "Piosenka: ", - "Channel Sponsor": "Sponsor kanału" + "Channel Sponsor": "Sponsor kanału", + "Standard YouTube license": "Standardowa licencja YouTube" } From d1393343765268f8131fab13cc8e95520f4f99ac Mon Sep 17 00:00:00 2001 From: Rex_sa Date: Mon, 20 Mar 2023 22:14:09 +0000 Subject: [PATCH 123/598] 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 3ce34c2d..fb9e7564 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -545,5 +545,6 @@ "Album: ": "الألبوم: ", "Artist: ": "الفنان: ", "Song: ": "أغنية: ", - "Channel Sponsor": "راعي القناة" + "Channel Sponsor": "راعي القناة", + "Standard YouTube license": "ترخيص YouTube القياسي" } From b97b5b5859ea0e0755745f7d2138e4d21ecaec0c Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Mon, 20 Mar 2023 20:03:55 +0000 Subject: [PATCH 124/598] Update Spanish translation --- locales/es.json | 75 +++++++++++++++++++++++++++++-------------------- 1 file changed, 45 insertions(+), 30 deletions(-) diff --git a/locales/es.json b/locales/es.json index bb082c06..aa03f124 100644 --- a/locales/es.json +++ b/locales/es.json @@ -398,37 +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}} visualización", - "generic_views_count_plural": "{{count}} visualizaciones", - "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 reproducción", + "generic_views_count_0": "{{count}} visualización", + "generic_views_count_1": "{{count}} visualizaciones", + "generic_views_count_2": "{{count}} visualizaciones", + "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 {{count}} respuestas", + "comments_view_x_replies_2": "Ver {{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": "{{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_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", @@ -469,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}} ficha", - "tokens_count_plural": "{{count}} fichas", + "tokens_count_0": "{{count}} ficha", + "tokens_count_1": "{{count}} fichas", + "tokens_count_2": "{{count}} fichas", "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 5c24bf1322f470ef46c1db34a24d4167f04ca987 Mon Sep 17 00:00:00 2001 From: Jorge Maldonado Ventura Date: Mon, 20 Mar 2023 18:46:59 +0000 Subject: [PATCH 125/598] 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 aa03f124..4f9250d4 100644 --- a/locales/es.json +++ b/locales/es.json @@ -497,5 +497,6 @@ "Artist: ": "Artista: ", "Album: ": "Álbum: ", "Song: ": "Canción: ", - "Channel Sponsor": "Patrocinador del canal" + "Channel Sponsor": "Patrocinador del canal", + "Standard YouTube license": "Licencia de YouTube estándar" } From 9eafbbdcbbcbdef86b5645219793f4e6cfe86a83 Mon Sep 17 00:00:00 2001 From: Jorge Maldonado Ventura Date: Mon, 20 Mar 2023 18:47:21 +0000 Subject: [PATCH 126/598] Update Esperanto translation --- locales/eo.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/locales/eo.json b/locales/eo.json index 9f37c7cb..a70b71d3 100644 --- a/locales/eo.json +++ b/locales/eo.json @@ -479,5 +479,8 @@ "channel_tab_shorts_label": "Mallongaj", "Music in this video": "Muziko en ĉi tiu video", "Artist: ": "Artisto: ", - "Album: ": "Albumo: " + "Album: ": "Albumo: ", + "Channel Sponsor": "Kanala sponsoro", + "Song: ": "Muzikaĵo: ", + "Standard YouTube license": "Implicita YouTube-licenco" } From ec1d6ee851953c52a0128b2ce303e3a9decf2b87 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Mon, 20 Mar 2023 19:35:31 +0000 Subject: [PATCH 127/598] 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 4d748e7f..e9c90287 100644 --- a/locales/uk.json +++ b/locales/uk.json @@ -497,5 +497,6 @@ "Artist: ": "Виконавець: ", "Album: ": "Альбом: ", "Song: ": "Пісня: ", - "Channel Sponsor": "Спонсор каналу" + "Channel Sponsor": "Спонсор каналу", + "Standard YouTube license": "Стандартна ліцензія YouTube" } From f46cc98654dc26ac58f61f856e6f4516f25031a7 Mon Sep 17 00:00:00 2001 From: Eric Date: Tue, 21 Mar 2023 03:48:53 +0000 Subject: [PATCH 128/598] 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 f202cf88..634175cb 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -465,5 +465,6 @@ "channel_tab_shorts_label": "短视频", "channel_tab_channels_label": "频道", "Song: ": "歌曲: ", - "Channel Sponsor": "频道赞助者" + "Channel Sponsor": "频道赞助者", + "Standard YouTube license": "标准 YouTube 许可证" } From 4aa2c406ff6f7f704d4243c7d7a79eaec0e4ec7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?O=C4=9Fuz=20Ersen?= Date: Mon, 20 Mar 2023 19:07:59 +0000 Subject: [PATCH 129/598] 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 6e0bc175..c5381f4c 100644 --- a/locales/tr.json +++ b/locales/tr.json @@ -481,5 +481,6 @@ "Music in this video": "Bu videodaki müzik", "Artist: ": "Sanatçı: ", "Channel Sponsor": "Kanal Sponsoru", - "Song: ": "Şarkı: " + "Song: ": "Şarkı: ", + "Standard YouTube license": "Standart YouTube lisansı" } From a9fcfcf7c9b15a7550349c76621b4006ce261a8c Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Wed, 22 Mar 2023 02:36:12 +0000 Subject: [PATCH 130/598] 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 54090d3d..4100931c 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -465,5 +465,6 @@ "Album: ": "專輯: ", "Music in this video": "此影片中的音樂", "Channel Sponsor": "頻道贊助者", - "Song: ": "歌曲: " + "Song: ": "歌曲: ", + "Standard YouTube license": "標準 YouTube 授權條款" } From 4078fc5818d27c0d4365f335e1ccbe0fd27c9f2f Mon Sep 17 00:00:00 2001 From: Parsa Date: Thu, 23 Mar 2023 07:24:05 +0000 Subject: [PATCH 131/598] Update Persian translation --- locales/fa.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/locales/fa.json b/locales/fa.json index 56685f64..29a0c527 100644 --- a/locales/fa.json +++ b/locales/fa.json @@ -450,5 +450,8 @@ "Music in this video": "آهنگ در این ویدیو", "Artist: ": "هنرمند: ", "Album: ": "آلبوم: ", - "Song: ": "آهنگ: " + "Song: ": "آهنگ: ", + "Channel Sponsor": "اسپانسر کانال", + "Standard YouTube license": "پروانه استاندارد YouTube", + "search_message_use_another_instance": " شما همچنین می‌توانید در نمونه دیگر هم جستجو کنید." } From a3e587657fdb62738115efda75d36bc078c93b77 Mon Sep 17 00:00:00 2001 From: Milo Ivir Date: Thu, 23 Mar 2023 19:43:36 +0000 Subject: [PATCH 132/598] 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 ade732ad..bbd899c9 100644 --- a/locales/hr.json +++ b/locales/hr.json @@ -497,5 +497,6 @@ "Album: ": "Album: ", "Artist: ": "Izvođač: ", "Channel Sponsor": "Sponzor kanala", - "Song: ": "Pjesma: " + "Song: ": "Pjesma: ", + "Standard YouTube license": "Standardna YouTube licenca" } From 1825b8edb3917000efaaeffd527c56e343c659c1 Mon Sep 17 00:00:00 2001 From: Fjuro Date: Tue, 21 Mar 2023 14:04:39 +0000 Subject: [PATCH 133/598] 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 4611c4fd..38c3a8d2 100644 --- a/locales/cs.json +++ b/locales/cs.json @@ -497,5 +497,6 @@ "Artist: ": "Umělec: ", "Album: ": "Album: ", "Channel Sponsor": "Sponzor kanálu", - "Song: ": "Skladba: " + "Song: ": "Skladba: ", + "Standard YouTube license": "Standardní licence YouTube" } From fe1648e72eca3e23fa7c4a7d811aee13ac230ba2 Mon Sep 17 00:00:00 2001 From: SC Date: Sat, 25 Mar 2023 11:38:22 +0000 Subject: [PATCH 134/598] Update Portuguese translation --- locales/pt.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/pt.json b/locales/pt.json index 310381ae..79d8f354 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -481,5 +481,6 @@ "Artist: ": "Artista: ", "Album: ": "Álbum: ", "Song: ": "Canção: ", - "Channel Sponsor": "Patrocinador do canal" + "Channel Sponsor": "Patrocinador do canal", + "Standard YouTube license": "Licença padrão do YouTube" } From 778edf63cb79ee7ff1d7fc1c20cef3d6f17a184e Mon Sep 17 00:00:00 2001 From: victor dargallo Date: Tue, 21 Mar 2023 20:27:50 +0000 Subject: [PATCH 135/598] Update Catalan translation --- locales/ca.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/locales/ca.json b/locales/ca.json index 54a0b177..59396c11 100644 --- a/locales/ca.json +++ b/locales/ca.json @@ -66,7 +66,7 @@ "Malay": "Malai", "Persian": "Persa", "Slovak": "Eslovac", - "Search": "Busca", + "Search": "Cerca", "Show annotations": "Mostra anotacions", "preferences_region_label": "País del contingut: ", "preferences_sort_label": "Ordena vídeos per: ", @@ -481,5 +481,6 @@ "Top": "Millors", "preferences_max_results_label": "Nombre de vídeos mostrats al feed: ", "Engagement: ": "Atracció: ", - "Redirect homepage to feed: ": "Redirigeix la pàgina d'inici al feed: " + "Redirect homepage to feed: ": "Redirigeix la pàgina d'inici al feed: ", + "Standard YouTube license": "Llicència estàndard de YouTube" } From 7b4e3639cf034abab14cc984f0cce30f698baacb Mon Sep 17 00:00:00 2001 From: Damjan Gerl Date: Tue, 21 Mar 2023 16:42:49 +0000 Subject: [PATCH 136/598] Update Slovenian translation --- locales/sl.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/locales/sl.json b/locales/sl.json index 47f295e0..ec1decaf 100644 --- a/locales/sl.json +++ b/locales/sl.json @@ -511,5 +511,8 @@ "channel_tab_streams_label": "Prenosi v živo", "Artist: ": "Umetnik/ca: ", "Music in this video": "Glasba v tem videoposnetku", - "Album: ": "Album: " + "Album: ": "Album: ", + "Song: ": "Pesem: ", + "Standard YouTube license": "Standardna licenca YouTube", + "Channel Sponsor": "Sponzor kanala" } From 231fb3481efda5a2e9b394dbe0cb35f3f9ae5793 Mon Sep 17 00:00:00 2001 From: maboroshin Date: Wed, 29 Mar 2023 01:13:39 +0000 Subject: [PATCH 137/598] Update Japanese translation --- locales/ja.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/ja.json b/locales/ja.json index 8a4537d4..d1813bcd 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -465,5 +465,6 @@ "Artist: ": "アーティスト: ", "Album: ": "アルバム: ", "Song: ": "曲: ", - "Channel Sponsor": "チャンネルのスポンサー" + "Channel Sponsor": "チャンネルのスポンサー", + "Standard YouTube license": "標準 Youtube ライセンス" } From d5a516d76cae1ec0b9bb89cb55b0fb884437b8a4 Mon Sep 17 00:00:00 2001 From: victor dargallo Date: Wed, 29 Mar 2023 20:35:14 +0000 Subject: [PATCH 138/598] Update Catalan translation --- locales/ca.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/ca.json b/locales/ca.json index 59396c11..efc8cc8a 100644 --- a/locales/ca.json +++ b/locales/ca.json @@ -75,7 +75,7 @@ "Title": "Títol", "Belarusian": "Bielorús", "Enable web notifications": "Activa notificacions web", - "search": "cerca", + "search": "Cerca", "Catalan": "Català", "Croatian": "Croat", "preferences_category_admin": "Preferències d'administrador", From 66e671237f74da17d251f4951e902fd211a484aa Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Sat, 1 Apr 2023 10:56:05 +0000 Subject: [PATCH 139/598] 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 4f9250d4..af82b2a3 100644 --- a/locales/es.json +++ b/locales/es.json @@ -498,5 +498,6 @@ "Album: ": "Álbum: ", "Song: ": "Canción: ", "Channel Sponsor": "Patrocinador del canal", - "Standard YouTube license": "Licencia de YouTube estándar" + "Standard YouTube license": "Licencia de YouTube estándar", + "Download is disabled": "La descarga está deshabilitada" } From d8337252a86dd0ad508a4d10cd6d68a9468f7c99 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Fri, 31 Mar 2023 21:24:47 +0000 Subject: [PATCH 140/598] 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 e9c90287..61bf3d31 100644 --- a/locales/uk.json +++ b/locales/uk.json @@ -498,5 +498,6 @@ "Album: ": "Альбом: ", "Song: ": "Пісня: ", "Channel Sponsor": "Спонсор каналу", - "Standard YouTube license": "Стандартна ліцензія YouTube" + "Standard YouTube license": "Стандартна ліцензія YouTube", + "Download is disabled": "Завантаження вимкнено" } From 9d52ddbf8decbec7cd2a34c75917ba08b46786d8 Mon Sep 17 00:00:00 2001 From: Eric Date: Sat, 1 Apr 2023 04:06:41 +0000 Subject: [PATCH 141/598] 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 634175cb..df31812a 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -466,5 +466,6 @@ "channel_tab_channels_label": "频道", "Song: ": "歌曲: ", "Channel Sponsor": "频道赞助者", - "Standard YouTube license": "标准 YouTube 许可证" + "Standard YouTube license": "标准 YouTube 许可证", + "Download is disabled": "已禁用下载" } From 657486c19ade93dd2ce45ea6b9fd68e3a5445cf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?O=C4=9Fuz=20Ersen?= Date: Fri, 31 Mar 2023 20:56:20 +0000 Subject: [PATCH 142/598] 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 c5381f4c..a2fdd573 100644 --- a/locales/tr.json +++ b/locales/tr.json @@ -482,5 +482,6 @@ "Artist: ": "Sanatçı: ", "Channel Sponsor": "Kanal Sponsoru", "Song: ": "Şarkı: ", - "Standard YouTube license": "Standart YouTube lisansı" + "Standard YouTube license": "Standart YouTube lisansı", + "Download is disabled": "İndirme devre dışı" } From d857ee5a7ca2082ab15be24130a14575f2e7485f Mon Sep 17 00:00:00 2001 From: Rex_sa Date: Sun, 2 Apr 2023 01:02:29 +0000 Subject: [PATCH 143/598] 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 fb9e7564..7303915b 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -546,5 +546,6 @@ "Artist: ": "الفنان: ", "Song: ": "أغنية: ", "Channel Sponsor": "راعي القناة", - "Standard YouTube license": "ترخيص YouTube القياسي" + "Standard YouTube license": "ترخيص YouTube القياسي", + "Download is disabled": "تم تعطيل التحميلات" } From c60c14851b5f950d49958fe5bf5fad6262662a91 Mon Sep 17 00:00:00 2001 From: Jorge Maldonado Ventura Date: Sun, 2 Apr 2023 19:53:43 +0000 Subject: [PATCH 144/598] 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 a70b71d3..464d16ca 100644 --- a/locales/eo.json +++ b/locales/eo.json @@ -482,5 +482,6 @@ "Album: ": "Albumo: ", "Channel Sponsor": "Kanala sponsoro", "Song: ": "Muzikaĵo: ", - "Standard YouTube license": "Implicita YouTube-licenco" + "Standard YouTube license": "Implicita YouTube-licenco", + "Download is disabled": "Elŝuto estas malebligita" } From 4c541489dd4dae5e5cb0761d3c993d6c40a24357 Mon Sep 17 00:00:00 2001 From: abyan akhtar Date: Sun, 2 Apr 2023 04:31:13 +0000 Subject: [PATCH 145/598] Update Indonesian translation --- locales/id.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/id.json b/locales/id.json index 51d6d55c..f0adfdb1 100644 --- a/locales/id.json +++ b/locales/id.json @@ -453,5 +453,6 @@ "crash_page_switch_instance": "mencoba untuk menggunakan peladen lainnya", "crash_page_read_the_faq": "baca Soal Sering Ditanya (SSD/FAQ)", "crash_page_search_issue": "mencari isu yang ada di GitHub", - "crash_page_report_issue": "Jika yang di atas tidak membantu, buka isu baru di GitHub (sebaiknya dalam bahasa Inggris) dan sertakan teks berikut dalam pesan Anda (JANGAN terjemahkan teks tersebut):" + "crash_page_report_issue": "Jika yang di atas tidak membantu, buka isu baru di GitHub (sebaiknya dalam bahasa Inggris) dan sertakan teks berikut dalam pesan Anda (JANGAN terjemahkan teks tersebut):", + "Popular enabled: ": "Populer diaktifkan: " } From f81bc96da08494263c187e37ed1a3bb39b570d95 Mon Sep 17 00:00:00 2001 From: Milo Ivir Date: Mon, 3 Apr 2023 16:37:54 +0000 Subject: [PATCH 146/598] 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 bbd899c9..b87a7729 100644 --- a/locales/hr.json +++ b/locales/hr.json @@ -498,5 +498,6 @@ "Artist: ": "Izvođač: ", "Channel Sponsor": "Sponzor kanala", "Song: ": "Pjesma: ", - "Standard YouTube license": "Standardna YouTube licenca" + "Standard YouTube license": "Standardna YouTube licenca", + "Download is disabled": "Preuzimanje je deaktivirano" } From e6ba3e3dab4ddf3034597ce03fc9f7cf421f3674 Mon Sep 17 00:00:00 2001 From: Fjuro Date: Mon, 3 Apr 2023 20:35:59 +0000 Subject: [PATCH 147/598] 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 38c3a8d2..0e8610bf 100644 --- a/locales/cs.json +++ b/locales/cs.json @@ -498,5 +498,6 @@ "Album: ": "Album: ", "Channel Sponsor": "Sponzor kanálu", "Song: ": "Skladba: ", - "Standard YouTube license": "Standardní licence YouTube" + "Standard YouTube license": "Standardní licence YouTube", + "Download is disabled": "Stahování je zakázáno" } From cb0e837a5ec7615e521ba86866f5e313583aa6a3 Mon Sep 17 00:00:00 2001 From: victor dargallo Date: Wed, 5 Apr 2023 13:55:57 +0000 Subject: [PATCH 148/598] Update Catalan translation --- locales/ca.json | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/locales/ca.json b/locales/ca.json index efc8cc8a..901249ac 100644 --- a/locales/ca.json +++ b/locales/ca.json @@ -122,8 +122,8 @@ "search_filters_features_option_location": "Ubicació", "search_filters_apply_button": "Aplica els filtres seleccionats", "videoinfo_started_streaming_x_ago": "Ha començat el directe fa `x`", - "next_steps_error_message_go_to_youtube": "Anar a YouTube", - "footer_donate_page": "Donar", + "next_steps_error_message_go_to_youtube": "Vés a YouTube", + "footer_donate_page": "Feu un donatiu", "footer_original_source_code": "Codi font original", "videoinfo_watch_on_youTube": "Veure a YouTube", "user_saved_playlists": "`x` llistes de reproducció guardades", @@ -164,7 +164,7 @@ "crash_page_report_issue": "Si cap de les anteriors no ha ajudat, obre un nou issue a GitHub (preferiblement en anglès) i inclou el text següent al missatge (NO tradueixis aquest text):", "generic_subscriptions_count": "{{count}} subscripció", "generic_subscriptions_count_plural": "{{count}} subscripcions", - "error_video_not_in_playlist": "El vídeo sol·licitat no existeix en aquesta llista de reproducció. Fes clic aquí per a la pàgina d'inici de la llista de reproducció.", + "error_video_not_in_playlist": "El vídeo sol·licitat no existeix en aquesta llista de reproducció. Feu clic aquí per a la pàgina d'inici de la llista de reproducció.", "comments_points_count": "{{count}} punt", "comments_points_count_plural": "{{count}} punts", "%A %B %-d, %Y": "%A %B %-d, %Y", @@ -175,7 +175,7 @@ "preferences_unseen_only_label": "Mostra només no vistos: ", "preferences_listen_label": "Escolta per defecte: ", "Import": "Importar", - "Token": "Senyal", + "Token": "Testimoni", "Wilson score: ": "Puntuació de Wilson: ", "search_filters_date_label": "Data de càrrega", "search_filters_features_option_three_sixty": "360°", @@ -184,10 +184,10 @@ "preferences_comments_label": "Comentaris per defecte: ", "`x` uploaded a video": "`x` ha penjat un vídeo", "Released under the AGPLv3 on Github.": "Publicat sota l'AGPLv3 a GitHub.", - "Token manager": "Gestor de tokens", + "Token manager": "Gestor de testimonis", "Watch history": "Historial de reproduccions", "Cannot change password for Google accounts": "No es pot canviar la contrasenya dels comptes de Google", - "Authorize token?": "Autoritzar senyal?", + "Authorize token?": "Autoritzar testimoni?", "Source available here.": "Font disponible aquí.", "Export subscriptions as OPML (for NewPipe & FreeTube)": "Exporta subscripcions com a OPML (per a NewPipe i FreeTube)", "Log in": "Inicia sessió", @@ -197,7 +197,7 @@ "Public": "Públic", "View all playlists": "Veure totes les llistes de reproducció", "reddit": "Reddit", - "Manage tokens": "Gestiona senyals", + "Manage tokens": "Gestiona testimonis", "Not a playlist.": "No és una llista de reproducció.", "preferences_local_label": "Vídeos de Proxy: ", "View channel on YouTube": "Veure canal a Youtube", @@ -272,7 +272,7 @@ "Khmer": "Khmer", "This channel does not exist.": "Aquest canal no existeix.", "Song: ": "Cançó: ", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "Error a l'iniciar sessió. Això pot ser perquè l'autenticació de dos factors no està activada per al vostre compte.", + "Login failed. This may be because two-factor authentication is not turned on for your account.": "S'ha produït un error en iniciar sessió. Això pot ser perquè l'autenticació de dos factors no està activada per al vostre compte.", "channel:`x`": "canal: `x`", "Deleted or invalid channel": "Canal suprimit o no vàlid", "Could not get channel info.": "No s'ha pogut obtenir la informació del canal.", @@ -298,10 +298,10 @@ "generic_views_count_plural": "{{count}} visualitzacions", "generic_videos_count": "{{count}} vídeo", "generic_videos_count_plural": "{{count}} vídeos", - "Token is expired, please try again": "La senyal ha caducat, torna-ho a provar", + "Token is expired, please try again": "El testimoni ha caducat, torna-ho a provar", "English": "Anglès", "Kannada": "Kanarès", - "Erroneous token": "Senyal errònia", + "Erroneous token": "Testimoni erroni", "`x` ago": "fa `x`", "Empty playlist": "Llista de reproducció buida", "Playlist does not exist.": "La llista de reproducció no existeix.", @@ -376,7 +376,7 @@ "Clear watch history": "Neteja l'historial de reproduccions", "Mongolian": "Mongol", "preferences_quality_dash_option_best": "Millor", - "Authorize token for `x`?": "Autoritzar senyal per a `x`?", + "Authorize token for `x`?": "Autoritzar testimoni per a `x`?", "Report statistics: ": "Estadístiques de l'informe: ", "Switch Invidious Instance": "Canvia la instància d'Invidious", "History": "Historial", @@ -410,7 +410,7 @@ "Export": "Exportar", "preferences_quality_dash_option_4320p": "4320p", "JavaScript license information": "Informació de la llicència de JavaScript", - "Hidden field \"token\" is a required field": "El camp ocult \"senyal\" és un camp obligatori", + "Hidden field \"token\" is a required field": "El camp ocult \"testimoni\" és un camp obligatori", "Shona": "Xona", "Family friendly? ": "Apte per a tots els públics? ", "preferences_quality_dash_label": "Qualitat de vídeo DASH preferida: ", @@ -443,7 +443,7 @@ "unsubscribe": "cancel·la la subscripció", "View playlist on YouTube": "Veure llista de reproducció a YouTube", "Import NewPipe subscriptions (.json)": "Importar subscripcions de NewPipe (.json)", - "crash_page_you_found_a_bug": "Sembla que has trobat un error a Invidious!", + "crash_page_you_found_a_bug": "Heu trobat un error a Invidious!", "Subscribe": "Subscriu-me", "Quota exceeded, try again in a few hours": "S'ha superat la quota, torna-ho a provar d'aquí a unes hores", "generic_count_days": "{{count}} dia", @@ -468,8 +468,8 @@ "revoke": "revocar", "English (United Kingdom)": "Anglès (Regne Unit)", "preferences_quality_option_hd720": "HD720", - "tokens_count": "{{count}} senyal", - "tokens_count_plural": "{{count}} senyals", + "tokens_count": "{{count}} testimoni", + "tokens_count_plural": "{{count}} testimonis", "subscriptions_unseen_notifs_count": "{{count}} notificació no vista", "subscriptions_unseen_notifs_count_plural": "{{count}} notificacions no vistes", "generic_subscribers_count": "{{count}} subscriptor", @@ -482,5 +482,6 @@ "preferences_max_results_label": "Nombre de vídeos mostrats al feed: ", "Engagement: ": "Atracció: ", "Redirect homepage to feed: ": "Redirigeix la pàgina d'inici al feed: ", - "Standard YouTube license": "Llicència estàndard de YouTube" + "Standard YouTube license": "Llicència estàndard de YouTube", + "Download is disabled": "Les baixades s'han inhabilitat" } From 6667bdcd92c998abc6aa594a79e6a4f164da98fd Mon Sep 17 00:00:00 2001 From: Damjan Gerl Date: Tue, 4 Apr 2023 12:16:45 +0000 Subject: [PATCH 149/598] Update Slovenian translation --- locales/sl.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/sl.json b/locales/sl.json index ec1decaf..410b432c 100644 --- a/locales/sl.json +++ b/locales/sl.json @@ -514,5 +514,6 @@ "Album: ": "Album: ", "Song: ": "Pesem: ", "Standard YouTube license": "Standardna licenca YouTube", - "Channel Sponsor": "Sponzor kanala" + "Channel Sponsor": "Sponzor kanala", + "Download is disabled": "Prenos je onemogočen" } From 919997e41c753ffec015783851abf6170d29dd9b Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Fri, 7 Apr 2023 02:25:36 +0000 Subject: [PATCH 150/598] 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 4100931c..daa22493 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -466,5 +466,6 @@ "Music in this video": "此影片中的音樂", "Channel Sponsor": "頻道贊助者", "Song: ": "歌曲: ", - "Standard YouTube license": "標準 YouTube 授權條款" + "Standard YouTube license": "標準 YouTube 授權條款", + "Download is disabled": "已停用下載" } From 72f83d4aa2004f3e43947622b2b5ac01601c9859 Mon Sep 17 00:00:00 2001 From: Andrey Date: Sat, 8 Apr 2023 08:18:06 +0000 Subject: [PATCH 151/598] Update Russian translation --- locales/ru.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/ru.json b/locales/ru.json index d2d7c86d..33a7931e 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -498,5 +498,6 @@ "Album: ": "Альбом: ", "Song: ": "Композиция: ", "Standard YouTube license": "Стандартная лицензия YouTube", - "Channel Sponsor": "Спонсор канала" + "Channel Sponsor": "Спонсор канала", + "Download is disabled": "Загрузка отключена" } From b9932b113bc5dedb924afdb3558151c6bd5a6347 Mon Sep 17 00:00:00 2001 From: atilluF Date: Sat, 8 Apr 2023 13:15:43 +0000 Subject: [PATCH 152/598] Update Italian translation --- locales/it.json | 86 ++++++++++++++++++++++++++++++------------------- 1 file changed, 53 insertions(+), 33 deletions(-) diff --git a/locales/it.json b/locales/it.json index c60f760b..5991304e 100644 --- a/locales/it.json +++ b/locales/it.json @@ -1,10 +1,13 @@ { - "generic_subscribers_count": "{{count}} iscritto", - "generic_subscribers_count_plural": "{{count}} iscritti", - "generic_videos_count": "{{count}} video", - "generic_videos_count_plural": "{{count}} video", - "generic_playlists_count": "{{count}} playlist", - "generic_playlists_count_plural": "{{count}} playlist", + "generic_subscribers_count_0": "{{count}} iscritto", + "generic_subscribers_count_1": "{{count}} iscritti", + "generic_subscribers_count_2": "{{count}} iscritti", + "generic_videos_count_0": "{{count}} video", + "generic_videos_count_1": "{{count}} video", + "generic_videos_count_2": "{{count}} video", + "generic_playlists_count_0": "{{count}} playlist", + "generic_playlists_count_1": "{{count}} playlist", + "generic_playlists_count_2": "{{count}} playlist", "LIVE": "IN DIRETTA", "Shared `x` ago": "Condiviso `x` fa", "Unsubscribe": "Disiscriviti", @@ -116,16 +119,19 @@ "Subscription manager": "Gestione delle iscrizioni", "Token manager": "Gestione dei gettoni", "Token": "Gettone", - "generic_subscriptions_count": "{{count}} iscrizione", - "generic_subscriptions_count_plural": "{{count}} iscrizioni", - "tokens_count": "{{count}} gettone", - "tokens_count_plural": "{{count}} gettoni", + "generic_subscriptions_count_0": "{{count}} iscrizione", + "generic_subscriptions_count_1": "{{count}} iscrizioni", + "generic_subscriptions_count_2": "{{count}} iscrizioni", + "tokens_count_0": "{{count}} gettone", + "tokens_count_1": "{{count}} gettoni", + "tokens_count_2": "{{count}} gettoni", "Import/export": "Importa/esporta", "unsubscribe": "disiscriviti", "revoke": "revoca", "Subscriptions": "Iscrizioni", - "subscriptions_unseen_notifs_count": "{{count}} notifica non visualizzata", - "subscriptions_unseen_notifs_count_plural": "{{count}} notifiche non visualizzate", + "subscriptions_unseen_notifs_count_0": "{{count}} notifica non visualizzata", + "subscriptions_unseen_notifs_count_1": "{{count}} notifiche non visualizzate", + "subscriptions_unseen_notifs_count_2": "{{count}} notifiche non visualizzate", "search": "Cerca", "Log out": "Esci", "Source available here.": "Codice sorgente.", @@ -154,8 +160,9 @@ "Whitelisted regions: ": "Regioni in lista bianca: ", "Blacklisted regions: ": "Regioni in lista nera: ", "Shared `x`": "Condiviso `x`", - "generic_views_count": "{{count}} visualizzazione", - "generic_views_count_plural": "{{count}} visualizzazioni", + "generic_views_count_0": "{{count}} visualizzazione", + "generic_views_count_1": "{{count}} visualizzazioni", + "generic_views_count_2": "{{count}} visualizzazioni", "Premieres in `x`": "In anteprima in `x`", "Premieres `x`": "In anteprima `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.": "Ciao, Sembra che tu abbia disattivato JavaScript. Clicca qui per visualizzare i commenti, ma considera che il caricamento potrebbe richiedere più tempo.", @@ -308,20 +315,27 @@ "Yiddish": "Yiddish", "Yoruba": "Yoruba", "Zulu": "Zulu", - "generic_count_years": "{{count}} anno", - "generic_count_years_plural": "{{count}} anni", - "generic_count_months": "{{count}} mese", - "generic_count_months_plural": "{{count}} mesi", - "generic_count_weeks": "{{count}} settimana", - "generic_count_weeks_plural": "{{count}} settimane", - "generic_count_days": "{{count}} giorno", - "generic_count_days_plural": "{{count}} giorni", - "generic_count_hours": "{{count}} ora", - "generic_count_hours_plural": "{{count}} ore", - "generic_count_minutes": "{{count}} minuto", - "generic_count_minutes_plural": "{{count}} minuti", - "generic_count_seconds": "{{count}} secondo", - "generic_count_seconds_plural": "{{count}} secondi", + "generic_count_years_0": "{{count}} anno", + "generic_count_years_1": "{{count}} anni", + "generic_count_years_2": "{{count}} anni", + "generic_count_months_0": "{{count}} mese", + "generic_count_months_1": "{{count}} mesi", + "generic_count_months_2": "{{count}} mesi", + "generic_count_weeks_0": "{{count}} settimana", + "generic_count_weeks_1": "{{count}} settimane", + "generic_count_weeks_2": "{{count}} settimane", + "generic_count_days_0": "{{count}} giorno", + "generic_count_days_1": "{{count}} giorni", + "generic_count_days_2": "{{count}} giorni", + "generic_count_hours_0": "{{count}} ora", + "generic_count_hours_1": "{{count}} ore", + "generic_count_hours_2": "{{count}} ore", + "generic_count_minutes_0": "{{count}} minuto", + "generic_count_minutes_1": "{{count}} minuti", + "generic_count_minutes_2": "{{count}} minuti", + "generic_count_seconds_0": "{{count}} secondo", + "generic_count_seconds_1": "{{count}} secondi", + "generic_count_seconds_2": "{{count}} secondi", "Fallback comments: ": "Commenti alternativi: ", "Popular": "Popolare", "Search": "Cerca", @@ -425,10 +439,12 @@ "search_filters_duration_option_short": "Corto (< 4 minuti)", "search_filters_duration_option_long": "Lungo (> 20 minuti)", "search_filters_features_option_purchased": "Acquistato", - "comments_view_x_replies": "Vedi {{count}} risposta", - "comments_view_x_replies_plural": "Vedi {{count}} risposte", - "comments_points_count": "{{count}} punto", - "comments_points_count_plural": "{{count}} punti", + "comments_view_x_replies_0": "Vedi {{count}} risposta", + "comments_view_x_replies_1": "Vedi {{count}} risposte", + "comments_view_x_replies_2": "Vedi {{count}} risposte", + "comments_points_count_0": "{{count}} punto", + "comments_points_count_1": "{{count}} punti", + "comments_points_count_2": "{{count}} punti", "Portuguese (auto-generated)": "Portoghese (generati automaticamente)", "crash_page_you_found_a_bug": "Sembra che tu abbia trovato un bug in Invidious!", "crash_page_switch_instance": "provato a usare un'altra istanza", @@ -479,5 +495,9 @@ "channel_tab_community_label": "Comunità", "Music in this video": "Musica in questo video", "Artist: ": "Artista: ", - "Album: ": "Album: " + "Album: ": "Album: ", + "Download is disabled": "Il download è disabilitato", + "Song: ": "Canzone: ", + "Standard YouTube license": "Licenza standard di YouTube", + "Channel Sponsor": "Sponsor del canale" } From 7d48b961733714b55f2d8d25a0bd254665176089 Mon Sep 17 00:00:00 2001 From: Ernestas Date: Sun, 9 Apr 2023 21:33:21 +0000 Subject: [PATCH 153/598] Update Lithuanian translation --- locales/lt.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/lt.json b/locales/lt.json index 9bfcfdba..91c7febe 100644 --- a/locales/lt.json +++ b/locales/lt.json @@ -488,5 +488,6 @@ "preferences_save_player_pos_label": "Išsaugoti atkūrimo padėtį: ", "videoinfo_youTube_embed_link": "Įterpti", "videoinfo_invidious_embed_link": "Įterpti nuorodą", - "crash_page_refresh": "pabandėte atnaujinti puslapį" + "crash_page_refresh": "pabandėte atnaujinti puslapį", + "Album: ": "Albumas " } From 346f32855a704efc23e46013376435cecc5f0462 Mon Sep 17 00:00:00 2001 From: SC Date: Mon, 10 Apr 2023 14:45:36 +0000 Subject: [PATCH 154/598] Update Portuguese translation --- locales/pt.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/pt.json b/locales/pt.json index 79d8f354..cbce0e5a 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -482,5 +482,6 @@ "Album: ": "Álbum: ", "Song: ": "Canção: ", "Channel Sponsor": "Patrocinador do canal", - "Standard YouTube license": "Licença padrão do YouTube" + "Standard YouTube license": "Licença padrão do YouTube", + "Download is disabled": "A descarga está desativada" } From 14053821ac7ac48f4ff276fbd12e3d29c4a2a917 Mon Sep 17 00:00:00 2001 From: AHOHNMYC Date: Sun, 16 Apr 2023 03:54:12 +0000 Subject: [PATCH 155/598] Update Russian translation --- locales/ru.json | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/locales/ru.json b/locales/ru.json index 33a7931e..4ce82bff 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -69,11 +69,11 @@ "preferences_vr_mode_label": "Интерактивные 360-градусные видео (необходим WebGL): ", "preferences_category_visual": "Настройки сайта", "preferences_player_style_label": "Стиль проигрывателя: ", - "Dark mode: ": "Темное оформление: ", + "Dark mode: ": "Тёмное оформление: ", "preferences_dark_mode_label": "Тема: ", - "dark": "темная", + "dark": "тёмная", "light": "светлая", - "preferences_thin_mode_label": "Облегченное оформление: ", + "preferences_thin_mode_label": "Облегчённое оформление: ", "preferences_category_misc": "Прочие настройки", "preferences_automatic_instance_redirect_label": "Автоматическая смена зеркала (переход на redirect.invidious.io): ", "preferences_category_subscription": "Настройки подписок", @@ -147,13 +147,13 @@ "License: ": "Лицензия: ", "Family friendly? ": "Семейный просмотр: ", "Wilson score: ": "Оценка Уилсона: ", - "Engagement: ": "Вовлеченность: ", + "Engagement: ": "Вовлечённость: ", "Whitelisted regions: ": "Доступно в регионах: ", "Blacklisted regions: ": "Недоступно в регионах: ", "Shared `x`": "Опубликовано `x`", "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. Нажмите сюда, чтобы увидеть комментарии. Но учтите: они могут загружаться немного медленнее.", + "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Похоже, у вас отключён JavaScript. Нажмите сюда, чтобы увидеть комментарии. Но учтите: они могут загружаться немного медленнее.", "View YouTube comments": "Показать комментарии с YouTube", "View more comments on Reddit": "Посмотреть больше комментариев на Reddit", "View `x` comments": { @@ -180,23 +180,23 @@ "Please log in": "Пожалуйста, войдите", "Invidious Private Feed for `x`": "Приватная лента Invidious для `x`", "channel:`x`": "канал: `x`", - "Deleted or invalid channel": "Канал удален или не найден", + "Deleted or invalid channel": "Канал удалён или не найден", "This channel does not exist.": "Такого канала не существует.", - "Could not get channel info.": "Не удается получить информацию об этом канале.", - "Could not fetch comments": "Не удается загрузить комментарии", + "Could not get channel info.": "Не удаётся получить информацию об этом канале.", + "Could not fetch comments": "Не удаётся загрузить комментарии", "`x` ago": "`x` назад", - "Load more": "Загрузить еще", + "Load more": "Загрузить ещё", "Could not create mix.": "Не удалось создать микс.", "Empty playlist": "Плейлист пуст", "Not a playlist.": "Это не плейлист.", "Playlist does not exist.": "Плейлист не существует.", - "Could not pull trending pages.": "Не удается загрузить страницы «в тренде».", + "Could not pull trending pages.": "Не удаётся загрузить страницы «в тренде».", "Hidden field \"challenge\" is a required field": "Необходимо заполнить скрытое поле «challenge»", "Hidden field \"token\" is a required field": "Необходимо заполнить скрытое поле «токен»", "Erroneous challenge": "Неправильный ответ в «challenge»", "Erroneous token": "Неправильный токен", "No such user": "Пользователь не найден", - "Token is expired, please try again": "Срок действия токена истек, попробуйте позже", + "Token is expired, please try again": "Срок действия токена истёк, попробуйте позже", "English": "Английский", "English (auto-generated)": "Английский (созданы автоматически)", "Afrikaans": "Африкаанс", @@ -379,7 +379,7 @@ "Turkish (auto-generated)": "Турецкий (созданы автоматически)", "Vietnamese (auto-generated)": "Вьетнамский (созданы автоматически)", "footer_documentation": "Документация", - "adminprefs_modified_source_code_url_label": "URL-адрес репозитория измененного исходного кода", + "adminprefs_modified_source_code_url_label": "Ссылка на репозиторий с измененными исходными кодами", "none": "ничего", "videoinfo_watch_on_youTube": "Смотреть на YouTube", "videoinfo_youTube_embed_link": "Версия для встраивания", @@ -453,8 +453,8 @@ "Portuguese (Brazil)": "Португальский (Бразилия)", "footer_source_code": "Исходный код", "footer_original_source_code": "Оригинальный исходный код", - "footer_modfied_source_code": "Измененный исходный код", - "user_saved_playlists": "`x` сохраненных плейлистов", + "footer_modfied_source_code": "Изменённый исходный код", + "user_saved_playlists": "`x` сохранённых плейлистов", "crash_page_search_issue": "поискали похожую проблему на GitHub", "comments_points_count_0": "{{count}} плюс", "comments_points_count_1": "{{count}} плюса", From 732fb7c499e3b3a9b5fcbd759d7d6edcc67d7772 Mon Sep 17 00:00:00 2001 From: John Donne Date: Sun, 16 Apr 2023 18:31:55 +0000 Subject: [PATCH 156/598] Update French translation --- locales/fr.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index 9d3e117f..d1093868 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -476,5 +476,7 @@ "channel_tab_shorts_label": "Clips", "channel_tab_streams_label": "En direct", "channel_tab_playlists_label": "Listes de lecture", - "channel_tab_channels_label": "Chaînes" + "channel_tab_channels_label": "Chaînes", + "Song: ": "Chanson : ", + "Artist: ": "Artiste : " } From 1f12323ee6ee35c90a9c9b145195b26c35b6321c Mon Sep 17 00:00:00 2001 From: Nicolas Dommanget-Muller Date: Sun, 16 Apr 2023 18:48:36 +0000 Subject: [PATCH 157/598] Update French translation --- locales/fr.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index d1093868..908ff5eb 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -478,5 +478,6 @@ "channel_tab_playlists_label": "Listes de lecture", "channel_tab_channels_label": "Chaînes", "Song: ": "Chanson : ", - "Artist: ": "Artiste : " + "Artist: ": "Artiste : ", + "Album: ": "Album: " } From 49e04192c04de8c0501c336d6bf6ef331ca193a4 Mon Sep 17 00:00:00 2001 From: John Donne Date: Sun, 16 Apr 2023 18:51:37 +0000 Subject: [PATCH 158/598] Update French translation --- locales/fr.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index 908ff5eb..bb40916b 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -474,10 +474,14 @@ "search_filters_duration_option_none": "Toutes les durées", "error_video_not_in_playlist": "La vidéo demandée n'existe pas dans cette liste de lecture. Cliquez ici pour retourner à la liste de lecture.", "channel_tab_shorts_label": "Clips", - "channel_tab_streams_label": "En direct", + "channel_tab_streams_label": "Vidéos en direct", "channel_tab_playlists_label": "Listes de lecture", "channel_tab_channels_label": "Chaînes", "Song: ": "Chanson : ", "Artist: ": "Artiste : ", - "Album: ": "Album: " + "Album: ": "Album : ", + "Standard YouTube license": "Licence YouTube Standard", + "Music in this video": "Musique dans cette vidéo", + "Channel Sponsor": "Soutien de la chaîne", + "Download is disabled": "Le téléchargement est désactivé" } From e6471feadc35674b8e987e645d3f52296c779a9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D1=91=D0=BC=20=D0=9A=D0=BE=D1=82=D0=BB?= =?UTF-8?q?=D1=83=D0=B1=D0=B0=D0=B9?= Date: Sun, 23 Apr 2023 09:02:34 +0000 Subject: [PATCH 159/598] Update Russian translation --- locales/ru.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/locales/ru.json b/locales/ru.json index 4ce82bff..1bece168 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -14,7 +14,7 @@ "Clear watch history?": "Очистить историю просмотров?", "New password": "Новый пароль", "New passwords must match": "Новые пароли не совпадают", - "Cannot change password for Google accounts": "Изменить пароль аккаунта Google невозможно", + "Cannot change password for Google accounts": "Изменить пароль учётной записи Google невозможно", "Authorize token?": "Авторизовать токен?", "Authorize token for `x`?": "Авторизовать токен для `x`?", "Yes": "Да", @@ -30,7 +30,7 @@ "Export subscriptions as OPML": "Экспортировать подписки в формате OPML", "Export subscriptions as OPML (for NewPipe & FreeTube)": "Экспортировать подписки в формате OPML (для NewPipe и FreeTube)", "Export data as JSON": "Экспортировать данные Invidious в формате JSON", - "Delete account?": "Удалить аккаунт?", + "Delete account?": "Удалить учётку?", "History": "История", "An alternative front-end to YouTube": "Альтернативный фронтенд для YouTube", "JavaScript license information": "Информация о лицензиях JavaScript", @@ -38,14 +38,14 @@ "Log in": "Войти", "Log in/register": "Войти или зарегистрироваться", "Log in with Google": "Войти через Google", - "User ID": "ID пользователя", + "User ID": "ИД пользователя", "Password": "Пароль", "Time (h:mm:ss):": "Время (ч:мм:сс):", "Text CAPTCHA": "Текстовая капча (англ.)", "Image CAPTCHA": "Капча-картинка", "Sign In": "Войти", "Register": "Зарегистрироваться", - "E-mail": "Электронная почта", + "E-mail": "Эл. почта", "Google verification code": "Код подтверждения Google", "Preferences": "Настройки", "preferences_category_player": "Настройки проигрывателя", @@ -171,7 +171,7 @@ "Wrong answer": "Неправильный ответ", "Erroneous CAPTCHA": "Неправильная капча", "CAPTCHA is a required field": "Необходимо решить капчу", - "User ID is a required field": "Необходимо ввести ID пользователя", + "User ID is a required field": "Необходимо ввести идентификатор пользователя", "Password is a required field": "Необходимо ввести пароль", "Wrong username or password": "Неправильный логин или пароль", "Please sign in using 'Log in with Google'": "Пожалуйста, нажмите «Войти через Google»", @@ -213,7 +213,7 @@ "Burmese": "Бирманский", "Catalan": "Каталонский", "Cebuano": "Себуанский", - "Chinese (Simplified)": "Китайский (упрощенный)", + "Chinese (Simplified)": "Китайский (упрощённый)", "Chinese (Traditional)": "Китайский (традиционный)", "Corsican": "Корсиканский", "Croatian": "Хорватский", From 70a79f343dd771be0842b9aef97edd357750a6fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D1=91=D0=BC=20=D0=9A=D0=BE=D1=82=D0=BB?= =?UTF-8?q?=D1=83=D0=B1=D0=B0=D0=B9?= Date: Mon, 24 Apr 2023 16:34:03 +0000 Subject: [PATCH 160/598] Update Russian translation --- locales/ru.json | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/locales/ru.json b/locales/ru.json index 1bece168..0031f79a 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -4,7 +4,7 @@ "Unsubscribe": "Отписаться", "Subscribe": "Подписаться", "View channel on YouTube": "Смотреть канал на YouTube", - "View playlist on YouTube": "Посмотреть плейлист на YouTube", + "View playlist on YouTube": "Просмотреть подборку на ютубе", "newest": "сначала новые", "oldest": "сначала старые", "popular": "популярные", @@ -129,14 +129,14 @@ "Public": "Публичный", "Unlisted": "Нет в списке", "Private": "Приватный", - "View all playlists": "Посмотреть все плейлисты", + "View all playlists": "Просмотреть все подборки", "Updated `x` ago": "Обновлено `x` назад", - "Delete playlist `x`?": "Удалить плейлист `x`?", - "Delete playlist": "Удалить плейлист", - "Create playlist": "Создать плейлист", + "Delete playlist `x`?": "Удалить подборку `x`?", + "Delete playlist": "Удалить подборку", + "Create playlist": "Создать подборку", "Title": "Заголовок", - "Playlist privacy": "Видимость плейлиста", - "Editing playlist `x`": "Редактирование плейлиста `x`", + "Playlist privacy": "Видимость подборки", + "Editing playlist `x`": "Изменение подборки `x`", "Show more": "Развернуть", "Show less": "Свернуть", "Watch on YouTube": "Смотреть на YouTube", @@ -187,9 +187,9 @@ "`x` ago": "`x` назад", "Load more": "Загрузить ещё", "Could not create mix.": "Не удалось создать микс.", - "Empty playlist": "Плейлист пуст", - "Not a playlist.": "Это не плейлист.", - "Playlist does not exist.": "Плейлист не существует.", + "Empty playlist": "Подборка пуста", + "Not a playlist.": "Это не подборка.", + "Playlist does not exist.": "Подборка не существует.", "Could not pull trending pages.": "Не удаётся загрузить страницы «в тренде».", "Hidden field \"challenge\" is a required field": "Необходимо заполнить скрытое поле «challenge»", "Hidden field \"token\" is a required field": "Необходимо заполнить скрытое поле «токен»", @@ -310,7 +310,7 @@ "About": "О сайте", "Rating: ": "Рейтинг: ", "preferences_locale_label": "Язык: ", - "View as playlist": "Смотреть как плейлист", + "View as playlist": "Смотреть как подборку", "Default": "По умолчанию", "Music": "Музыка", "Gaming": "Игры", @@ -326,7 +326,7 @@ "Audio mode": "Аудио режим", "Video mode": "Видео режим", "channel_tab_videos_label": "Видео", - "Playlists": "Плейлисты", + "Playlists": "Подборки", "channel_tab_community_label": "Сообщество", "search_filters_sort_option_relevance": "по актуальности", "search_filters_sort_option_rating": "по рейтингу", @@ -343,7 +343,7 @@ "search_filters_date_option_year": "Этот год", "search_filters_type_option_video": "Видео", "search_filters_type_option_channel": "Канал", - "search_filters_type_option_playlist": "Плейлист", + "search_filters_type_option_playlist": "Подборка", "search_filters_type_option_movie": "Фильм", "search_filters_type_option_show": "Сериал", "search_filters_features_option_hd": "HD", @@ -385,7 +385,7 @@ "videoinfo_youTube_embed_link": "Версия для встраивания", "videoinfo_invidious_embed_link": "Ссылка для встраивания", "download_subtitles": "Субтитры - `x` (.vtt)", - "user_created_playlists": "`x` созданных плейлистов", + "user_created_playlists": "`x` созданных подборок", "crash_page_you_found_a_bug": "Похоже, вы нашли ошибку в Invidious!", "crash_page_before_reporting": "Прежде чем сообщать об ошибке, убедитесь, что вы:", "crash_page_refresh": "пробовали перезагрузить страницу", @@ -393,9 +393,9 @@ "generic_videos_count_0": "{{count}} видео", "generic_videos_count_1": "{{count}} видео", "generic_videos_count_2": "{{count}} видео", - "generic_playlists_count_0": "{{count}} плейлист", - "generic_playlists_count_1": "{{count}} плейлиста", - "generic_playlists_count_2": "{{count}} плейлистов", + "generic_playlists_count_0": "{{count}} подборка", + "generic_playlists_count_1": "{{count}} подборки", + "generic_playlists_count_2": "{{count}} подборок", "tokens_count_0": "{{count}} токен", "tokens_count_1": "{{count}} токена", "tokens_count_2": "{{count}} токенов", @@ -454,7 +454,7 @@ "footer_source_code": "Исходный код", "footer_original_source_code": "Оригинальный исходный код", "footer_modfied_source_code": "Изменённый исходный код", - "user_saved_playlists": "`x` сохранённых плейлистов", + "user_saved_playlists": "`x` сохранённых подборок", "crash_page_search_issue": "поискали похожую проблему на GitHub", "comments_points_count_0": "{{count}} плюс", "comments_points_count_1": "{{count}} плюса", @@ -488,8 +488,8 @@ "search_filters_duration_option_medium": "Средние (4 - 20 минут)", "search_filters_apply_button": "Применить фильтры", "Popular enabled: ": "Популярное включено: ", - "error_video_not_in_playlist": "Запрошенного видео нет в этом плейлисте. Нажмите тут, чтобы вернуться к странице плейлиста.", - "channel_tab_playlists_label": "Плейлисты", + "error_video_not_in_playlist": "Запрошенного видео нет в этой подборке. Нажмите тут, чтобы вернуться к странице подборки.", + "channel_tab_playlists_label": "Подборки", "channel_tab_channels_label": "Каналы", "channel_tab_streams_label": "Живое вещание", "channel_tab_shorts_label": "Shorts", From deed4d10f28574500891bc5c5892799a09cded6c Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sun, 30 Apr 2023 19:31:59 +0200 Subject: [PATCH 161/598] Fix broken Spanish/Italian locales (i18next v3->v4 mixup) --- locales/es.json | 75 +++++++++++++++++++--------------------------- locales/it.json | 80 ++++++++++++++++++++----------------------------- 2 files changed, 62 insertions(+), 93 deletions(-) diff --git a/locales/es.json b/locales/es.json index af82b2a3..09f510a7 100644 --- a/locales/es.json +++ b/locales/es.json @@ -398,51 +398,37 @@ "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}} visualización", - "generic_views_count_1": "{{count}} visualizaciones", - "generic_views_count_2": "{{count}} visualizaciones", - "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 {{count}} respuestas", - "comments_view_x_replies_2": "Ver {{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_views_count": "{{count}} visualización", + "generic_views_count_plural": "{{count}} visualizaciones", + "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 reproducción", "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_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 +469,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}} ficha", - "tokens_count_1": "{{count}} fichas", - "tokens_count_2": "{{count}} fichas", + "tokens_count": "{{count}} ficha", + "tokens_count_plural": "{{count}} fichas", "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.", diff --git a/locales/it.json b/locales/it.json index 5991304e..0797b387 100644 --- a/locales/it.json +++ b/locales/it.json @@ -1,13 +1,10 @@ { - "generic_subscribers_count_0": "{{count}} iscritto", - "generic_subscribers_count_1": "{{count}} iscritti", - "generic_subscribers_count_2": "{{count}} iscritti", - "generic_videos_count_0": "{{count}} video", - "generic_videos_count_1": "{{count}} video", - "generic_videos_count_2": "{{count}} video", - "generic_playlists_count_0": "{{count}} playlist", - "generic_playlists_count_1": "{{count}} playlist", - "generic_playlists_count_2": "{{count}} playlist", + "generic_subscribers_count": "{{count}} iscritto", + "generic_subscribers_count_plural": "{{count}} iscritti", + "generic_videos_count": "{{count}} video", + "generic_videos_count_plural": "{{count}} video", + "generic_playlists_count": "{{count}} playlist", + "generic_playlists_count_plural": "{{count}} playlist", "LIVE": "IN DIRETTA", "Shared `x` ago": "Condiviso `x` fa", "Unsubscribe": "Disiscriviti", @@ -119,19 +116,16 @@ "Subscription manager": "Gestione delle iscrizioni", "Token manager": "Gestione dei gettoni", "Token": "Gettone", - "generic_subscriptions_count_0": "{{count}} iscrizione", - "generic_subscriptions_count_1": "{{count}} iscrizioni", - "generic_subscriptions_count_2": "{{count}} iscrizioni", - "tokens_count_0": "{{count}} gettone", - "tokens_count_1": "{{count}} gettoni", - "tokens_count_2": "{{count}} gettoni", + "generic_subscriptions_count": "{{count}} iscrizione", + "generic_subscriptions_count_plural": "{{count}} iscrizioni", + "tokens_count": "{{count}} gettone", + "tokens_count_plural": "{{count}} gettoni", "Import/export": "Importa/esporta", "unsubscribe": "disiscriviti", "revoke": "revoca", "Subscriptions": "Iscrizioni", - "subscriptions_unseen_notifs_count_0": "{{count}} notifica non visualizzata", - "subscriptions_unseen_notifs_count_1": "{{count}} notifiche non visualizzate", - "subscriptions_unseen_notifs_count_2": "{{count}} notifiche non visualizzate", + "subscriptions_unseen_notifs_count": "{{count}} notifica non visualizzata", + "subscriptions_unseen_notifs_count_plural": "{{count}} notifiche non visualizzate", "search": "Cerca", "Log out": "Esci", "Source available here.": "Codice sorgente.", @@ -160,9 +154,8 @@ "Whitelisted regions: ": "Regioni in lista bianca: ", "Blacklisted regions: ": "Regioni in lista nera: ", "Shared `x`": "Condiviso `x`", - "generic_views_count_0": "{{count}} visualizzazione", - "generic_views_count_1": "{{count}} visualizzazioni", - "generic_views_count_2": "{{count}} visualizzazioni", + "generic_views_count": "{{count}} visualizzazione", + "generic_views_count_plural": "{{count}} visualizzazioni", "Premieres in `x`": "In anteprima in `x`", "Premieres `x`": "In anteprima `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.": "Ciao, Sembra che tu abbia disattivato JavaScript. Clicca qui per visualizzare i commenti, ma considera che il caricamento potrebbe richiedere più tempo.", @@ -315,27 +308,20 @@ "Yiddish": "Yiddish", "Yoruba": "Yoruba", "Zulu": "Zulu", - "generic_count_years_0": "{{count}} anno", - "generic_count_years_1": "{{count}} anni", - "generic_count_years_2": "{{count}} anni", - "generic_count_months_0": "{{count}} mese", - "generic_count_months_1": "{{count}} mesi", - "generic_count_months_2": "{{count}} mesi", - "generic_count_weeks_0": "{{count}} settimana", - "generic_count_weeks_1": "{{count}} settimane", - "generic_count_weeks_2": "{{count}} settimane", - "generic_count_days_0": "{{count}} giorno", - "generic_count_days_1": "{{count}} giorni", - "generic_count_days_2": "{{count}} giorni", - "generic_count_hours_0": "{{count}} ora", - "generic_count_hours_1": "{{count}} ore", - "generic_count_hours_2": "{{count}} ore", - "generic_count_minutes_0": "{{count}} minuto", - "generic_count_minutes_1": "{{count}} minuti", - "generic_count_minutes_2": "{{count}} minuti", - "generic_count_seconds_0": "{{count}} secondo", - "generic_count_seconds_1": "{{count}} secondi", - "generic_count_seconds_2": "{{count}} secondi", + "generic_count_years": "{{count}} anno", + "generic_count_years_plural": "{{count}} anni", + "generic_count_months": "{{count}} mese", + "generic_count_months_plural": "{{count}} mesi", + "generic_count_weeks": "{{count}} settimana", + "generic_count_weeks_plural": "{{count}} settimane", + "generic_count_days": "{{count}} giorno", + "generic_count_days_plural": "{{count}} giorni", + "generic_count_hours": "{{count}} ora", + "generic_count_hours_plural": "{{count}} ore", + "generic_count_minutes": "{{count}} minuto", + "generic_count_minutes_plural": "{{count}} minuti", + "generic_count_seconds": "{{count}} secondo", + "generic_count_seconds_plural": "{{count}} secondi", "Fallback comments: ": "Commenti alternativi: ", "Popular": "Popolare", "Search": "Cerca", @@ -439,12 +425,10 @@ "search_filters_duration_option_short": "Corto (< 4 minuti)", "search_filters_duration_option_long": "Lungo (> 20 minuti)", "search_filters_features_option_purchased": "Acquistato", - "comments_view_x_replies_0": "Vedi {{count}} risposta", - "comments_view_x_replies_1": "Vedi {{count}} risposte", - "comments_view_x_replies_2": "Vedi {{count}} risposte", - "comments_points_count_0": "{{count}} punto", - "comments_points_count_1": "{{count}} punti", - "comments_points_count_2": "{{count}} punti", + "comments_view_x_replies": "Vedi {{count}} risposta", + "comments_view_x_replies_plural": "Vedi {{count}} risposte", + "comments_points_count": "{{count}} punto", + "comments_points_count_plural": "{{count}} punti", "Portuguese (auto-generated)": "Portoghese (generati automaticamente)", "crash_page_you_found_a_bug": "Sembra che tu abbia trovato un bug in Invidious!", "crash_page_switch_instance": "provato a usare un'altra istanza", From f298e225a114578e0551e04d5e68f2bfcbe84e72 Mon Sep 17 00:00:00 2001 From: chunky programmer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Mon, 24 Apr 2023 19:29:34 -0400 Subject: [PATCH 162/598] fix live video attachments, parse playlists --- src/invidious/channels/community.cr | 68 ++++++--------------- src/invidious/helpers/serialized_yt_data.cr | 1 + src/invidious/yt_backend/extractors.cr | 2 +- 3 files changed, 22 insertions(+), 49 deletions(-) diff --git a/src/invidious/channels/community.cr b/src/invidious/channels/community.cr index ad786f3a..87430305 100644 --- a/src/invidious/channels/community.cr +++ b/src/invidious/channels/community.cr @@ -123,49 +123,13 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode) if attachment = post["backstageAttachment"]? json.field "attachment" do - json.object do - case attachment.as_h - when .has_key?("videoRenderer") - attachment = attachment["videoRenderer"] - json.field "type", "video" - - if !attachment["videoId"]? - error_message = (attachment["title"]["simpleText"]? || - attachment["title"]["runs"]?.try &.[0]?.try &.["text"]?) - - json.field "error", error_message - else - video_id = attachment["videoId"].as_s - - video_title = attachment["title"]["simpleText"]? || attachment["title"]["runs"]?.try &.[0]?.try &.["text"]? - json.field "title", video_title - json.field "videoId", video_id - json.field "videoThumbnails" do - Invidious::JSONify::APIv1.thumbnails(json, video_id) - end - - json.field "lengthSeconds", decode_length_seconds(attachment["lengthText"]["simpleText"].as_s) - - author_info = attachment["ownerText"]["runs"][0].as_h - - json.field "author", author_info["text"].as_s - json.field "authorId", author_info["navigationEndpoint"]["browseEndpoint"]["browseId"] - json.field "authorUrl", author_info["navigationEndpoint"]["commandMetadata"]["webCommandMetadata"]["url"] - - # TODO: json.field "authorThumbnails", "channelThumbnailSupportedRenderers" - # TODO: json.field "authorVerified", "ownerBadges" - - published = decode_date(attachment["publishedTimeText"]["simpleText"].as_s) - - json.field "published", published.to_unix - json.field "publishedText", translate(locale, "`x` ago", recode_date(published, locale)) - - view_count = attachment["viewCountText"]?.try &.["simpleText"].as_s.gsub(/\D/, "").to_i64? || 0_i64 - - json.field "viewCount", view_count - json.field "viewCountText", translate_count(locale, "generic_views_count", view_count, NumberFormatting::Short) - end - when .has_key?("backstageImageRenderer") + case attachment.as_h + when .has_key?("videoRenderer") + parse_item(attachment) + .as(SearchVideo) + .to_json(locale, json) + when .has_key?("backstageImageRenderer") + json.object do attachment = attachment["backstageImageRenderer"] json.field "type", "image" @@ -186,7 +150,9 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode) end end end - when .has_key?("pollRenderer") + end + when .has_key?("pollRenderer") + json.object do attachment = attachment["pollRenderer"] json.field "type", "poll" json.field "totalVotes", short_text_to_number(attachment["totalVotes"]["simpleText"].as_s.split(" ")[0]) @@ -219,7 +185,9 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode) end end end - when .has_key?("postMultiImageRenderer") + end + when .has_key?("postMultiImageRenderer") + json.object do attachment = attachment["postMultiImageRenderer"] json.field "type", "multiImage" json.field "images" do @@ -243,10 +211,14 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode) end end end - else - json.field "type", "unknown" - json.field "error", "Unrecognized attachment type." end + when .has_key?("playlistRenderer") + parse_item(attachment) + .as(SearchPlaylist) + .to_json(locale, json) + else + json.field "type", "unknown" + json.field "error", "Unrecognized attachment type." end end end diff --git a/src/invidious/helpers/serialized_yt_data.cr b/src/invidious/helpers/serialized_yt_data.cr index c1874780..7c12ad0e 100644 --- a/src/invidious/helpers/serialized_yt_data.cr +++ b/src/invidious/helpers/serialized_yt_data.cr @@ -84,6 +84,7 @@ struct SearchVideo json.field "descriptionHtml", self.description_html json.field "viewCount", self.views + json.field "viewCountText", translate_count(locale, "generic_views_count", self.views, NumberFormatting::Short) json.field "published", self.published.to_unix json.field "publishedText", translate(locale, "`x` ago", recode_date(self.published, locale)) json.field "lengthSeconds", self.length_seconds diff --git a/src/invidious/yt_backend/extractors.cr b/src/invidious/yt_backend/extractors.cr index 9c041361..8ff4c1f9 100644 --- a/src/invidious/yt_backend/extractors.cr +++ b/src/invidious/yt_backend/extractors.cr @@ -268,7 +268,7 @@ private module Parsers end private def self.parse(item_contents, author_fallback) - title = item_contents["title"]["simpleText"]?.try &.as_s || "" + title = extract_text(item_contents["title"]) || "" plid = item_contents["playlistId"]?.try &.as_s || "" video_count = HelperExtractors.get_video_count(item_contents) From d420741cc15dce656da641f5143120ec88e59bc8 Mon Sep 17 00:00:00 2001 From: chunky programmer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Wed, 19 Apr 2023 20:59:06 -0400 Subject: [PATCH 163/598] Allow channel urls to be displayed in YT description --- src/invidious/videos/description.cr | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/invidious/videos/description.cr b/src/invidious/videos/description.cr index 2017955d..0a9d84f8 100644 --- a/src/invidious/videos/description.cr +++ b/src/invidious/videos/description.cr @@ -6,13 +6,19 @@ def parse_command(command : JSON::Any?, string : String) : String? # 3rd party URL, extract original URL from YouTube tracking URL if url_endpoint = on_tap.try &.["urlEndpoint"]? - youtube_url = URI.parse url_endpoint["url"].as_s - - original_url = youtube_url.query_params["q"]? - if original_url.nil? - return "" + if url_endpoint["url"].as_s.includes? "youtube.com/redirect" + youtube_url = URI.parse url_endpoint["url"].as_s + original_url = youtube_url.query_params["q"]? + if original_url.nil? + return "" + else + return "#{original_url}" + end else - return "#{original_url}" + # not a redirect url, some first party url + # see https://github.com/iv-org/invidious/issues/3751 + first_party_url = url_endpoint["url"].as_s + return "#{first_party_url}" end # 1st party watch URL elsif watch_endpoint = on_tap.try &.["watchEndpoint"]? From 1b10446e5ecfb50d84fae88b6b8953ed19bfe1fb Mon Sep 17 00:00:00 2001 From: chunky programmer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Mon, 24 Apr 2023 17:40:58 -0400 Subject: [PATCH 164/598] move url parsing to utils method --- src/invidious/comments.cr | 51 +------------------------ src/invidious/helpers/utils.cr | 53 ++++++++++++++++++++++++++ src/invidious/videos/description.cr | 59 +++-------------------------- src/invidious/videos/parser.cr | 2 +- 4 files changed, 62 insertions(+), 103 deletions(-) diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr index fd2be73d..0c863977 100644 --- a/src/invidious/comments.cr +++ b/src/invidious/comments.cr @@ -635,55 +635,8 @@ def content_to_comment_html(content, video_id : String? = "") text = HTML.escape(run["text"].as_s) - if run["navigationEndpoint"]? - if url = run["navigationEndpoint"]["urlEndpoint"]?.try &.["url"].as_s - url = URI.parse(url) - displayed_url = text - - if url.host == "youtu.be" - url = "/watch?v=#{url.request_target.lstrip('/')}" - elsif url.host.nil? || url.host.not_nil!.ends_with?("youtube.com") - if url.path == "/redirect" - # Sometimes, links can be corrupted (why?) so make sure to fallback - # nicely. See https://github.com/iv-org/invidious/issues/2682 - url = url.query_params["q"]? || "" - displayed_url = url - else - url = url.request_target - displayed_url = "youtube.com#{url}" - end - end - - text = %(#{reduce_uri(displayed_url)}) - elsif watch_endpoint = run["navigationEndpoint"]["watchEndpoint"]? - start_time = watch_endpoint["startTimeSeconds"]?.try &.as_i - link_video_id = watch_endpoint["videoId"].as_s - - url = "/watch?v=#{link_video_id}" - url += "&t=#{start_time}" if !start_time.nil? - - # If the current video ID (passed through from the caller function) - # is the same as the video ID in the link, add HTML attributes for - # the JS handler function that bypasses page reload. - # - # See: https://github.com/iv-org/invidious/issues/3063 - if link_video_id == video_id - start_time ||= 0 - text = %(#{reduce_uri(text)}) - else - text = %(#{text}) - end - elsif url = run.dig?("navigationEndpoint", "commandMetadata", "webCommandMetadata", "url").try &.as_s - if text.starts_with?(/\s?[@#]/) - # Handle "pings" in comments and hasthags differently - # See: - # - https://github.com/iv-org/invidious/issues/3038 - # - https://github.com/iv-org/invidious/issues/3062 - text = %(#{text}) - else - text = %(#{reduce_uri(url)}) - end - end + if navigationEndpoint = run.dig?("navigationEndpoint") + text = parse_link_endpoint(navigationEndpoint, text, video_id) end text = "#{text}" if run["bold"]? diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index 500a2582..bcf7c963 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -389,3 +389,56 @@ def reduce_uri(uri : URI | String, max_length : Int32 = 50, suffix : String = " end return str end + +# Get the html link from a NavigationEndpoint or an innertubeCommand +def parse_link_endpoint(endpoint : JSON::Any, text : String, video_id : String) + if url = endpoint.dig?("urlEndpoint", "url").try &.as_s + url = URI.parse(url) + displayed_url = text + + if url.host == "youtu.be" + url = "/watch?v=#{url.request_target.lstrip('/')}" + elsif url.host.nil? || url.host.not_nil!.ends_with?("youtube.com") + if url.path == "/redirect" + # Sometimes, links can be corrupted (why?) so make sure to fallback + # nicely. See https://github.com/iv-org/invidious/issues/2682 + url = url.query_params["q"]? || "" + displayed_url = url + else + url = url.request_target + displayed_url = "youtube.com#{url}" + end + end + + text = %(#{reduce_uri(displayed_url)}) + elsif watch_endpoint = endpoint.dig?("watchEndpoint") + start_time = watch_endpoint["startTimeSeconds"]?.try &.as_i + link_video_id = watch_endpoint["videoId"].as_s + + url = "/watch?v=#{link_video_id}" + url += "&t=#{start_time}" if !start_time.nil? + + # If the current video ID (passed through from the caller function) + # is the same as the video ID in the link, add HTML attributes for + # the JS handler function that bypasses page reload. + # + # See: https://github.com/iv-org/invidious/issues/3063 + if link_video_id == video_id + start_time ||= 0 + text = %(#{reduce_uri(text)}) + else + text = %(#{text}) + end + elsif url = endpoint.dig?("commandMetadata", "webCommandMetadata", "url").try &.as_s + if text.starts_with?(/\s?[@#]/) + # Handle "pings" in comments and hasthags differently + # See: + # - https://github.com/iv-org/invidious/issues/3038 + # - https://github.com/iv-org/invidious/issues/3062 + text = %(#{text}) + else + text = %(#{reduce_uri(url)}) + end + end + return text +end diff --git a/src/invidious/videos/description.cr b/src/invidious/videos/description.cr index 0a9d84f8..542cb416 100644 --- a/src/invidious/videos/description.cr +++ b/src/invidious/videos/description.cr @@ -1,57 +1,6 @@ require "json" require "uri" -def parse_command(command : JSON::Any?, string : String) : String? - on_tap = command.dig?("onTap", "innertubeCommand") - - # 3rd party URL, extract original URL from YouTube tracking URL - if url_endpoint = on_tap.try &.["urlEndpoint"]? - if url_endpoint["url"].as_s.includes? "youtube.com/redirect" - youtube_url = URI.parse url_endpoint["url"].as_s - original_url = youtube_url.query_params["q"]? - if original_url.nil? - return "" - else - return "#{original_url}" - end - else - # not a redirect url, some first party url - # see https://github.com/iv-org/invidious/issues/3751 - first_party_url = url_endpoint["url"].as_s - return "#{first_party_url}" - end - # 1st party watch URL - elsif watch_endpoint = on_tap.try &.["watchEndpoint"]? - video_id = watch_endpoint["videoId"].as_s - time = watch_endpoint["startTimeSeconds"].as_i - - url = "/watch?v=#{video_id}&t=#{time}s" - - # if string is a timestamp, use the string instead - # this is a lazy regex for validating timestamps - if /(?:\d{1,2}:){1,2}\d{2}/ =~ string - return "#{string}" - else - return "#{url}" - end - # hashtag/other browse URLs - elsif browse_endpoint = on_tap.try &.dig?("commandMetadata", "webCommandMetadata") - url = browse_endpoint["url"].try &.as_s - - # remove unnecessary character in a channel name - if browse_endpoint["webPageType"]?.try &.as_s == "WEB_PAGE_TYPE_CHANNEL" - name = string.match(/@[\w\d.-]+/) - if name.try &.[0]? - return "#{name.try &.[0]}" - end - end - - return "#{string}" - end - - return "(unknown YouTube desc command)" -end - private def copy_string(str : String::Builder, iter : Iterator, count : Int) : Int copied = 0 while copied < count @@ -68,7 +17,7 @@ private def copy_string(str : String::Builder, iter : Iterator, count : Int) : I return copied end -def parse_description(desc : JSON::Any?) : String? +def parse_description(desc, video_id : String) : String? return "" if desc.nil? content = desc["content"].as_s @@ -100,7 +49,11 @@ def parse_description(desc : JSON::Any?) : String? copy_string(str2, iter, cmd_length) end - str << parse_command(command, cmd_content) + link = cmd_content + if on_tap = command.dig?("onTap", "innertubeCommand") + link = parse_link_endpoint(on_tap, cmd_content, video_id) + end + str << link index += cmd_length end diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr index 1c6d118d..2e8eecc3 100644 --- a/src/invidious/videos/parser.cr +++ b/src/invidious/videos/parser.cr @@ -287,7 +287,7 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any # description_html = video_secondary_renderer.try &.dig?("description", "runs") # .try &.as_a.try { |t| content_to_comment_html(t, video_id) } - description_html = parse_description(video_secondary_renderer.try &.dig?("attributedDescription")) + description_html = parse_description(video_secondary_renderer.try &.dig?("attributedDescription"), video_id) # Video metadata From 28584f22c52b243da740061eeb834e300f36b7c1 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 11 Apr 2023 20:50:23 -0400 Subject: [PATCH 165/598] Fix index out of bounds error --- src/invidious/comments.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr index fd2be73d..b5815bb4 100644 --- a/src/invidious/comments.cr +++ b/src/invidious/comments.cr @@ -604,7 +604,7 @@ def text_to_parsed_content(text : String) : JSON::Any currentNode = {"text" => urlMatch[0], "navigationEndpoint" => {"urlEndpoint" => {"url" => urlMatch[0]}}} currentNodes << (JSON.parse(currentNode.to_json)) # If text remain after match create new simple node with text after match - afterNode = {"text" => splittedLastNode.size > 0 ? splittedLastNode[1] : ""} + afterNode = {"text" => splittedLastNode.size > 1 ? splittedLastNode[1] : ""} currentNodes << (JSON.parse(afterNode.to_json)) end From 384a8e200c953ed5be3ba6a01762e933fd566e45 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Tue, 2 May 2023 23:18:40 +0200 Subject: [PATCH 166/598] Trending: fix mistakes from #3773 --- src/invidious/trending.cr | 18 ++++++++++++++++-- src/invidious/yt_backend/extractors_utils.cr | 13 +++++++------ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/invidious/trending.cr b/src/invidious/trending.cr index 74bab1bd..fcaf60d1 100644 --- a/src/invidious/trending.cr +++ b/src/invidious/trending.cr @@ -20,6 +20,20 @@ def fetch_trending(trending_type, region, locale) items, _ = extract_items(initial_data) - # Return items, but ignore categories (e.g featured content) - return items.reject!(Category), plid + extracted = [] of SearchItem + + items.each do |itm| + if itm.is_a?(Category) + # Ignore the smaller categories, as they generally contain a sponsored + # channel, which brings a lot of noise on the trending page. + # See: https://github.com/iv-org/invidious/issues/2989 + next if itm.contents.size < 24 + + extracted.concat extract_category(itm) + else + extracted << itm + end + end + + return extracted, plid end diff --git a/src/invidious/yt_backend/extractors_utils.cr b/src/invidious/yt_backend/extractors_utils.cr index b247dca8..11d95958 100644 --- a/src/invidious/yt_backend/extractors_utils.cr +++ b/src/invidious/yt_backend/extractors_utils.cr @@ -68,16 +68,17 @@ rescue ex return false end -# This function extracts the SearchItems from a Category. +# This function extracts SearchVideo items from a Category. # Categories are commonly returned in search results and trending pages. def extract_category(category : Category) : Array(SearchVideo) - items = [] of SearchItem + return category.contents.select(SearchVideo) +end - category.contents.each do |item| - target << cate_i if item.is_a?(SearchItem) +# :ditto: +def extract_category(category : Category, &) + category.contents.select(SearchVideo).each do |item| + yield item end - - return items end def extract_selected_tab(tabs) From 90914343ec1a4c89e8bb873fdefa0a8e8ac656df Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Wed, 3 May 2023 00:02:38 +0200 Subject: [PATCH 167/598] Trending: de-duplicate results --- src/invidious/trending.cr | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/invidious/trending.cr b/src/invidious/trending.cr index fcaf60d1..2d9f8a83 100644 --- a/src/invidious/trending.cr +++ b/src/invidious/trending.cr @@ -35,5 +35,6 @@ def fetch_trending(trending_type, region, locale) end end - return extracted, plid + # Deduplicate items before returning results + return extracted.select(SearchVideo).uniq!(&.id), plid end From 2d5145614be46c0b59a87c26cecac0c4b69e3437 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 2 May 2023 21:10:57 -0400 Subject: [PATCH 168/598] Fix unknown type attachment Co-authored-by: Samantaz Fox --- src/invidious/channels/community.cr | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/invidious/channels/community.cr b/src/invidious/channels/community.cr index 87430305..2c7b9fec 100644 --- a/src/invidious/channels/community.cr +++ b/src/invidious/channels/community.cr @@ -217,8 +217,10 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode) .as(SearchPlaylist) .to_json(locale, json) else - json.field "type", "unknown" - json.field "error", "Unrecognized attachment type." + json.object do + json.field "type", "unknown" + json.field "error", "Unrecognized attachment type." + end end end end From 7aac401407627fef167b2d0f5bb3dd2324de6a1c Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 6 May 2023 19:23:55 +0200 Subject: [PATCH 169/598] CSS: limit width of the comments in community tab --- assets/css/default.css | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/assets/css/default.css b/assets/css/default.css index 42f6958f..4d06b77f 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -321,6 +321,16 @@ p.channel-name { margin: 0; } p.video-data { margin: 0; font-weight: bold; font-size: 80%; } +/* + * Comments & community posts + */ + +#comments { + max-width: 800px; + margin: auto; +} + + /* * Footer */ From ce2649420fb868596bd926393fb1073d2671a4f5 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 6 May 2023 19:36:52 +0200 Subject: [PATCH 170/598] CSS: Fix iframe attachment size in community posts --- assets/css/default.css | 14 ++++++++++++++ src/invidious/comments.cr | 18 +++++------------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/assets/css/default.css b/assets/css/default.css index 4d06b77f..23649f8f 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -330,6 +330,20 @@ p.video-data { margin: 0; font-weight: bold; font-size: 80%; } margin: auto; } +.video-iframe-wrapper { + position: relative; + height: 0; + padding-bottom: 56.25%; +} + +.video-iframe { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + border: none; +} /* * Footer diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr index ec4449f0..f43e39c6 100644 --- a/src/invidious/comments.cr +++ b/src/invidious/comments.cr @@ -372,27 +372,19 @@ def template_youtube_comments(comments, locale, thin_mode, is_replies = false)
    END_HTML when "video" - html << <<-END_HTML -
    -
    -
    - END_HTML - if attachment["error"]? html << <<-END_HTML +

    #{attachment["error"]}

    +
    END_HTML else html << <<-END_HTML - +
    + +
    END_HTML end - - html << <<-END_HTML -
    -
    -
    - END_HTML else nil # Ignore end end From 720789b6221518fd1614debfcee794a422df9466 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 6 May 2023 19:41:07 +0200 Subject: [PATCH 171/598] HTML: wrap comments metadata in a paragraph --- src/invidious/comments.cr | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr index f43e39c6..01556099 100644 --- a/src/invidious/comments.cr +++ b/src/invidious/comments.cr @@ -390,6 +390,7 @@ def template_youtube_comments(comments, locale, thin_mode, is_replies = false) 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 @@ -408,6 +409,7 @@ def template_youtube_comments(comments, locale, thin_mode, is_replies = false) html << <<-END_HTML #{number_with_separator(child["likeCount"])} +

    END_HTML if child["creatorHeart"]? From 36f7c99cfb96dd743df237a09c390e11cedae420 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89milien=20Devos=20=28perso=29?= Date: Sun, 7 May 2023 17:49:43 +0200 Subject: [PATCH 172/598] Update config.example.yml Document save playback position in the config.example.yml --- config/config.example.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/config/config.example.yml b/config/config.example.yml index 8abe1b9e..7ea80017 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -817,6 +817,16 @@ default_user_preferences: ## Default: true ## #vr_mode: true + + ## + ## Save the playback position + ## Allow to continue watching at the previous position when + ## watching the same video. + ## + ## Accepted values: true, false + ## Default: false + ## + #save_player_pos: false # ----------------------------- # Subscription feed From f3d9db10a2be1c8ef3f9b919343dde6a6f36fcb0 Mon Sep 17 00:00:00 2001 From: Fjuro Date: Mon, 1 May 2023 12:59:27 +0000 Subject: [PATCH 173/598] Update Czech translation --- locales/cs.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/cs.json b/locales/cs.json index 0e8610bf..d9e5b4d5 100644 --- a/locales/cs.json +++ b/locales/cs.json @@ -13,7 +13,7 @@ "Previous page": "Předchozí strana", "Clear watch history?": "Smazat historii?", "New password": "Nové heslo", - "New passwords must match": "Hesla se musí schodovat", + "New passwords must match": "Hesla se musí shodovat", "Cannot change password for Google accounts": "Nelze změnit heslo pro účty Google", "Authorize token?": "Autorizovat token?", "Authorize token for `x`?": "Autorizovat token pro `x`?", From cca8bcf2a85fe7c2e241cb26ec94ee51dd8f37e7 Mon Sep 17 00:00:00 2001 From: xrfmkrh Date: Mon, 1 May 2023 05:57:30 +0000 Subject: [PATCH 174/598] Update Korean translation --- locales/ko.json | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/locales/ko.json b/locales/ko.json index d4f3a711..2b454add 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -46,7 +46,7 @@ "Log in/register": "로그인/회원가입", "Log in": "로그인", "source": "출처", - "JavaScript license information": "자바스크립트 라이센스 정보", + "JavaScript license information": "자바스크립트 라이선스 정보", "An alternative front-end to YouTube": "유튜브의 프론트엔드 대안", "History": "역사", "Delete account?": "계정을 삭제 하시겠습니까?", @@ -116,7 +116,7 @@ "Show replies": "댓글 보기", "Hide replies": "댓글 숨기기", "Incorrect password": "잘못된 비밀번호", - "License: ": "라이센스: ", + "License: ": "라이선스: ", "Genre: ": "장르: ", "Editing playlist `x`": "재생목록 `x` 수정하기", "Playlist privacy": "재생목록 공개 범위", @@ -135,7 +135,7 @@ "Unlisted": "목록에 없음", "Public": "공개", "View privacy policy.": "개인정보 처리방침 보기.", - "View JavaScript license information.": "자바스크립트 라이센스 정보 보기.", + "View JavaScript license information.": "자바스크립트 라이선스 정보 보기.", "Source available here.": "소스는 여기에서 사용할 수 있습니다.", "Log out": "로그아웃", "search": "검색", @@ -460,5 +460,12 @@ "channel_tab_shorts_label": "쇼츠", "channel_tab_streams_label": "실시간 스트리밍", "channel_tab_channels_label": "채널", - "channel_tab_playlists_label": "재생목록" + "channel_tab_playlists_label": "재생목록", + "Standard YouTube license": "표준 유튜브 라이선스", + "Song: ": "제목: ", + "Channel Sponsor": "채널 스폰서", + "Album: ": "앨범: ", + "Music in this video": "동영상 속 음악", + "Artist: ": "아티스트: ", + "Download is disabled": "다운로드가 비활성화 되어있음" } From 56ebb477caff6189da5db40291d68c6e895fc2d8 Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Wed, 3 May 2023 12:25:54 +0000 Subject: [PATCH 175/598] Update Spanish translation --- locales/es.json | 75 +++++++++++++++++++++++++++++-------------------- 1 file changed, 45 insertions(+), 30 deletions(-) diff --git a/locales/es.json b/locales/es.json index 09f510a7..63079d9e 100644 --- a/locales/es.json +++ b/locales/es.json @@ -398,37 +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}} visualización", - "generic_views_count_plural": "{{count}} visualizaciones", - "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 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 sin ver", + "subscriptions_unseen_notifs_count_1": "{{count}} notificaciones sin ver", + "subscriptions_unseen_notifs_count_2": "{{count}} notificaciones sin ver", + "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 {{count}} respuestas", + "comments_view_x_replies_2": "Ver {{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}} reproducción", + "generic_playlists_count_1": "{{count}} reproducciones", + "generic_playlists_count_2": "{{count}} reproducciones", "generic_videos_count_0": "{{count}} video", "generic_videos_count_1": "{{count}} videos", "generic_videos_count_2": "{{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_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", @@ -469,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}} ficha", - "tokens_count_plural": "{{count}} fichas", + "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 ce1fb8d08c86f747ee638289c8bcfeb208702445 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Mon, 8 May 2023 00:53:08 +0200 Subject: [PATCH 176/598] Use XML.parse instead of XML.parse_html Due to recent changes to libxml2 (between 2.9.14 and 2.10.4, See https://gitlab.gnome.org/GNOME/libxml2/-/issues/508), the HTML parser doesn't take into account the namespaces (xmlns). Because HTML shouldn't contain namespaces anyway, there is no reason for use to keep using it. But switching to the XML parser means that we have to pass the namespaces to every single 'xpath_node(s)' method for it to be able to properly navigate the XML structure. --- src/invidious/channels/channels.cr | 36 +++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/src/invidious/channels/channels.cr b/src/invidious/channels/channels.cr index 63dd2194..b09d93b1 100644 --- a/src/invidious/channels/channels.cr +++ b/src/invidious/channels/channels.cr @@ -159,12 +159,18 @@ def fetch_channel(ucid, pull_all_videos : Bool) LOGGER.debug("fetch_channel: #{ucid}") LOGGER.trace("fetch_channel: #{ucid} : pull_all_videos = #{pull_all_videos}") + namespaces = { + "yt" => "http://www.youtube.com/xml/schemas/2015", + "media" => "http://search.yahoo.com/mrss/", + "default" => "http://www.w3.org/2005/Atom", + } + LOGGER.trace("fetch_channel: #{ucid} : Downloading RSS feed") rss = YT_POOL.client &.get("/feeds/videos.xml?channel_id=#{ucid}").body LOGGER.trace("fetch_channel: #{ucid} : Parsing RSS feed") - rss = XML.parse_html(rss) + rss = XML.parse(rss) - author = rss.xpath_node(%q(//feed/title)) + author = rss.xpath_node("//default:feed/default:title", namespaces) if !author raise InfoException.new("Deleted or invalid channel") end @@ -192,15 +198,23 @@ def fetch_channel(ucid, pull_all_videos : Bool) videos, continuation = IV::Channel::Tabs.get_videos(channel) LOGGER.trace("fetch_channel: #{ucid} : Extracting videos from channel RSS feed") - rss.xpath_nodes("//feed/entry").each do |entry| - video_id = entry.xpath_node("videoid").not_nil!.content - title = entry.xpath_node("title").not_nil!.content - published = Time.parse_rfc3339(entry.xpath_node("published").not_nil!.content) - updated = Time.parse_rfc3339(entry.xpath_node("updated").not_nil!.content) - author = entry.xpath_node("author/name").not_nil!.content - ucid = entry.xpath_node("channelid").not_nil!.content - views = entry.xpath_node("group/community/statistics").try &.["views"]?.try &.to_i64? - views ||= 0_i64 + rss.xpath_nodes("//default:feed/default:entry", namespaces).each do |entry| + video_id = entry.xpath_node("yt:videoid", namespaces).not_nil!.content + title = entry.xpath_node("default:title", namespaces).not_nil!.content + + published = Time.parse_rfc3339( + entry.xpath_node("default:published", namespaces).not_nil!.content + ) + updated = Time.parse_rfc3339( + entry.xpath_node("default:updated", namespaces).not_nil!.content + ) + + author = entry.xpath_node("default:author/default:name", namespaces).not_nil!.content + ucid = entry.xpath_node("yt:channelid", namespaces).not_nil!.content + + views = entry + .xpath_node("media:group/media:community/media:statistics", namespaces) + .try &.["views"]?.try &.to_i64? || 0_i64 channel_video = videos .select(SearchVideo) From c385a944e642ce9e060c2dcf2082ecf0bb10b45a Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Mon, 8 May 2023 13:10:18 +0200 Subject: [PATCH 177/598] Subscriptions: Fix casing of XML tag names --- src/invidious/channels/channels.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/invidious/channels/channels.cr b/src/invidious/channels/channels.cr index b09d93b1..c3d6124f 100644 --- a/src/invidious/channels/channels.cr +++ b/src/invidious/channels/channels.cr @@ -199,7 +199,7 @@ def fetch_channel(ucid, pull_all_videos : Bool) LOGGER.trace("fetch_channel: #{ucid} : Extracting videos from channel RSS feed") rss.xpath_nodes("//default:feed/default:entry", namespaces).each do |entry| - video_id = entry.xpath_node("yt:videoid", namespaces).not_nil!.content + video_id = entry.xpath_node("yt:videoId", namespaces).not_nil!.content title = entry.xpath_node("default:title", namespaces).not_nil!.content published = Time.parse_rfc3339( @@ -210,7 +210,7 @@ def fetch_channel(ucid, pull_all_videos : Bool) ) author = entry.xpath_node("default:author/default:name", namespaces).not_nil!.content - ucid = entry.xpath_node("yt:channelid", namespaces).not_nil!.content + ucid = entry.xpath_node("yt:channelId", namespaces).not_nil!.content views = entry .xpath_node("media:group/media:community/media:statistics", namespaces) From 544fc9f92e9f3a55e362282680542b6ecbebfdc3 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Mon, 8 May 2023 15:33:23 +0200 Subject: [PATCH 178/598] 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 63079d9e..68ff0170 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 sin ver", - "subscriptions_unseen_notifs_count_1": "{{count}} notificaciones sin ver", - "subscriptions_unseen_notifs_count_2": "{{count}} notificaciones sin ver", - "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 {{count}} respuestas", - "comments_view_x_replies_2": "Ver {{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}} reproducción", - "generic_playlists_count_1": "{{count}} reproducciones", - "generic_playlists_count_2": "{{count}} 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 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", "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 6755e31b726fa857e75ede988af216a52eab8cc7 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sun, 14 May 2023 20:10:56 +0200 Subject: [PATCH 179/598] Fix hashtag continuation token --- src/invidious/hashtag.cr | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/invidious/hashtag.cr b/src/invidious/hashtag.cr index bc329205..d9d584c9 100644 --- a/src/invidious/hashtag.cr +++ b/src/invidious/hashtag.cr @@ -17,21 +17,18 @@ module Invidious::Hashtag "80226972:embedded" => { "2:string" => "FEhashtag", "3:base64" => { - "1:varint" => cursor.to_i64, - }, - "7:base64" => { - "325477796:embedded" => { - "1:embedded" => { - "2:0:embedded" => { - "2:string" => '#' + hashtag, - "4:varint" => 0_i64, - "11:string" => "", - }, - "4:string" => "browse-feedFEhashtag", - }, - "2:string" => hashtag, + "1:varint" => 60_i64, # result count + "15:base64" => { + "1:varint" => cursor.to_i64, + "2:varint" => 0_i64, + }, + "93:2:embedded" => { + "1:string" => hashtag, + "2:varint" => 0_i64, + "3:varint" => 1_i64, }, }, + "35:string" => "browse-feedFEhashtag", }, } 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 180/598] 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 181/598] 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 b2a0e6f1ffe448f8c3f6f943b34c673537210794 Mon Sep 17 00:00:00 2001 From: chunky programmer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Sun, 14 May 2023 16:49:49 -0400 Subject: [PATCH 182/598] Parse playlists when searching a channel --- src/invidious/yt_backend/extractors.cr | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/invidious/yt_backend/extractors.cr b/src/invidious/yt_backend/extractors.cr index 8ff4c1f9..6686e6e7 100644 --- a/src/invidious/yt_backend/extractors.cr +++ b/src/invidious/yt_backend/extractors.cr @@ -381,7 +381,7 @@ private module Parsers # Parses an InnerTube itemSectionRenderer into a SearchVideo. # Returns nil when the given object isn't a ItemSectionRenderer # - # A itemSectionRenderer seems to be a simple wrapper for a videoRenderer, used + # A itemSectionRenderer seems to be a simple wrapper for a videoRenderer or a playlistRenderer, used # by the result page for channel searches. It is located inside a continuationItems # container.It is very similar to RichItemRendererParser # @@ -394,6 +394,8 @@ private module Parsers private def self.parse(item_contents, author_fallback) child = VideoRendererParser.process(item_contents, author_fallback) + child ||= PlaylistRendererParser.process(item_contents, author_fallback) + return child end 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 183/598] 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 c713c32cebda5d0199b5c0dd553744f8d61707da Mon Sep 17 00:00:00 2001 From: chunky programmer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Sun, 14 May 2023 22:35:51 -0400 Subject: [PATCH 184/598] Fix issue where playlists will refetch the same videos --- src/invidious/routes/playlists.cr | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/invidious/routes/playlists.cr b/src/invidious/routes/playlists.cr index 0d242ee6..8675fa45 100644 --- a/src/invidious/routes/playlists.cr +++ b/src/invidious/routes/playlists.cr @@ -410,8 +410,8 @@ module Invidious::Routes::Playlists return error_template(500, ex) end - page_count = (playlist.video_count / 100).to_i - page_count += 1 if (playlist.video_count % 100) > 0 + page_count = (playlist.video_count / 200).to_i + page_count += 1 if (playlist.video_count % 200) > 0 if page > page_count return env.redirect "/playlist?list=#{plid}&page=#{page_count}" @@ -422,7 +422,7 @@ module Invidious::Routes::Playlists end begin - videos = get_playlist_videos(playlist, offset: (page - 1) * 100) + videos = get_playlist_videos(playlist, offset: (page - 1) * 200) rescue ex return error_template(500, "Error encountered while retrieving playlist videos.
    #{ex.message}") 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 185/598] 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 186/598] 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 187/598] 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 188/598] 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 189/598] 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 190/598] 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 191/598] 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 192/598] 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 193/598] 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 194/598] 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 195/598] 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 196/598] 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 197/598] 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 198/598] 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 199/598] 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 200/598] 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 201/598] 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 202/598] 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 203/598] 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 204/598] 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 205/598] 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 206/598] 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 207/598] 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 208/598] 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 209/598] 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 210/598] 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 211/598] 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 212/598] 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 213/598] 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 214/598] 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" From 898066407d85a2844c87fa6fc0e8179977cabb9c Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Mon, 29 May 2023 12:41:53 +0200 Subject: [PATCH 215/598] Utils: Update 'decode_date' to take into account short "x ago" forms --- src/invidious/helpers/utils.cr | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index bcf7c963..48bf769f 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -111,24 +111,27 @@ def decode_date(string : String) else nil # Continue end - # String matches format "20 hours ago", "4 months ago"... - date = string.split(" ")[-3, 3] - delta = date[0].to_i + # String matches format "20 hours ago", "4 months ago", "20s ago", "15min ago"... + match = string.match(/(?\d+) ?(?[smhdwy]\w*) ago/) - case date[1] - when .includes? "second" + raise "Could not parse #{string}" if match.nil? + + delta = match["count"].to_i + + case match["span"] + when .starts_with? "s" # second(s) delta = delta.seconds - when .includes? "minute" + when .starts_with? "mi" # minute(s) delta = delta.minutes - when .includes? "hour" + when .starts_with? "h" # hour(s) delta = delta.hours - when .includes? "day" + when .starts_with? "d" # day(s) delta = delta.days - when .includes? "week" + when .starts_with? "w" # week(s) delta = delta.weeks - when .includes? "month" + when .starts_with? "mo" # month(s) delta = delta.months - when .includes? "year" + when .starts_with? "y" # year(s) delta = delta.years else raise "Could not parse #{string}" From 4414c9df70580008c8817ace026b765e83c052aa Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Mon, 29 May 2023 12:42:19 +0200 Subject: [PATCH 216/598] specc: Add tests for 'decode_date' --- spec/invidious/utils_spec.cr | 46 ++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 spec/invidious/utils_spec.cr diff --git a/spec/invidious/utils_spec.cr b/spec/invidious/utils_spec.cr new file mode 100644 index 00000000..7c2c2711 --- /dev/null +++ b/spec/invidious/utils_spec.cr @@ -0,0 +1,46 @@ +require "../spec_helper" + +Spectator.describe "Utils" do + describe "decode_date" do + it "parses short dates (en-US)" do + expect(decode_date("1s ago")).to be_close(Time.utc - 1.second, 500.milliseconds) + expect(decode_date("2min ago")).to be_close(Time.utc - 2.minutes, 500.milliseconds) + expect(decode_date("3h ago")).to be_close(Time.utc - 3.hours, 500.milliseconds) + expect(decode_date("4d ago")).to be_close(Time.utc - 4.days, 500.milliseconds) + expect(decode_date("5w ago")).to be_close(Time.utc - 5.weeks, 500.milliseconds) + expect(decode_date("6mo ago")).to be_close(Time.utc - 6.months, 500.milliseconds) + expect(decode_date("7y ago")).to be_close(Time.utc - 7.years, 500.milliseconds) + end + + it "parses short dates (en-GB)" do + expect(decode_date("55s ago")).to be_close(Time.utc - 55.seconds, 500.milliseconds) + expect(decode_date("44min ago")).to be_close(Time.utc - 44.minutes, 500.milliseconds) + expect(decode_date("22hr ago")).to be_close(Time.utc - 22.hours, 500.milliseconds) + expect(decode_date("1day ago")).to be_close(Time.utc - 1.day, 500.milliseconds) + expect(decode_date("2days ago")).to be_close(Time.utc - 2.days, 500.milliseconds) + expect(decode_date("3wk ago")).to be_close(Time.utc - 3.weeks, 500.milliseconds) + expect(decode_date("11mo ago")).to be_close(Time.utc - 11.months, 500.milliseconds) + expect(decode_date("11yr ago")).to be_close(Time.utc - 11.years, 500.milliseconds) + end + + it "parses long forms (singular)" do + expect(decode_date("1 second ago")).to be_close(Time.utc - 1.second, 500.milliseconds) + expect(decode_date("1 minute ago")).to be_close(Time.utc - 1.minute, 500.milliseconds) + expect(decode_date("1 hour ago")).to be_close(Time.utc - 1.hour, 500.milliseconds) + expect(decode_date("1 day ago")).to be_close(Time.utc - 1.day, 500.milliseconds) + expect(decode_date("1 week ago")).to be_close(Time.utc - 1.week, 500.milliseconds) + expect(decode_date("1 month ago")).to be_close(Time.utc - 1.month, 500.milliseconds) + expect(decode_date("1 year ago")).to be_close(Time.utc - 1.year, 500.milliseconds) + end + + it "parses long forms (plural)" do + expect(decode_date("5 seconds ago")).to be_close(Time.utc - 5.seconds, 500.milliseconds) + expect(decode_date("17 minutes ago")).to be_close(Time.utc - 17.minutes, 500.milliseconds) + expect(decode_date("23 hours ago")).to be_close(Time.utc - 23.hours, 500.milliseconds) + expect(decode_date("3 days ago")).to be_close(Time.utc - 3.days, 500.milliseconds) + expect(decode_date("2 weeks ago")).to be_close(Time.utc - 2.weeks, 500.milliseconds) + expect(decode_date("9 months ago")).to be_close(Time.utc - 9.months, 500.milliseconds) + expect(decode_date("8 years ago")).to be_close(Time.utc - 8.years, 500.milliseconds) + end + end +end From 042ad1f2662503c123ba1dd415e5ed3d9ddc3cc0 Mon Sep 17 00:00:00 2001 From: Emilien Devos Date: Sat, 3 Jun 2023 13:06:48 +0200 Subject: [PATCH 217/598] auto close duplicated issues --- .github/workflows/auto-close-duplicate.yaml | 35 +++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/auto-close-duplicate.yaml diff --git a/.github/workflows/auto-close-duplicate.yaml b/.github/workflows/auto-close-duplicate.yaml new file mode 100644 index 00000000..3e977a84 --- /dev/null +++ b/.github/workflows/auto-close-duplicate.yaml @@ -0,0 +1,35 @@ +name: Close duplicates +on: + issues: + types: [opened] +jobs: + run: + runs-on: ubuntu-latest + permissions: write-all + steps: + - uses: iv-org/close-potential-duplicates@v1 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Issue title filter work with anymatch https://www.npmjs.com/package/anymatch. + # Any matched issue will stop detection immediately. + # You can specify multi filters in each line. + filter: '' + # Exclude keywords in title before detecting. + exclude: '' + # Label to set, when potential duplicates are detected. + label: duplicate + # Get issues with state to compare. Supported state: 'all', 'closed', 'open'. + state: open + # If similarity is higher than this threshold([0,1]), issue will be marked as duplicate. + threshold: 0.6 + # Reactions to be add to comment when potential duplicates are detected. + # Available reactions: "-1", "+1", "confused", "laugh", "heart", "hooray", "rocket", "eyes" + reactions: '' + close: true + # Comment to post when potential duplicates are detected. + comment: > + Hello, your issue is a duplicate of this/these issue(s): {{#issues}} + - #{{ number }} [accuracy: ({{ accuracy }}%)] + {{/issues}} + If this is a mistake please explain why and ping @\unixfox, @\SamantazFox and @\TheFrenchGhosty. + Please refrain from opening new issues, it won't help in solving your problem. \ No newline at end of file From 7ea6ec1f52ae02cbc35401ad272433d7073d8866 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89milien=20Devos=20=28perso=29?= Date: Sat, 3 Jun 2023 18:57:42 +0200 Subject: [PATCH 218/598] add one return line for the reply message --- .github/workflows/auto-close-duplicate.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/auto-close-duplicate.yaml b/.github/workflows/auto-close-duplicate.yaml index 3e977a84..4495c22e 100644 --- a/.github/workflows/auto-close-duplicate.yaml +++ b/.github/workflows/auto-close-duplicate.yaml @@ -32,4 +32,5 @@ jobs: - #{{ number }} [accuracy: ({{ accuracy }}%)] {{/issues}} If this is a mistake please explain why and ping @\unixfox, @\SamantazFox and @\TheFrenchGhosty. + Please refrain from opening new issues, it won't help in solving your problem. \ No newline at end of file From bc06c2fc27a9f1fb4edb8a2af570d67c0af5ba0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89milien=20Devos=20=28perso=29?= Date: Sat, 3 Jun 2023 17:27:24 +0000 Subject: [PATCH 219/598] Better message for auto close --- .github/workflows/auto-close-duplicate.yaml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/auto-close-duplicate.yaml b/.github/workflows/auto-close-duplicate.yaml index 4495c22e..aa6457ed 100644 --- a/.github/workflows/auto-close-duplicate.yaml +++ b/.github/workflows/auto-close-duplicate.yaml @@ -27,10 +27,11 @@ jobs: reactions: '' close: true # Comment to post when potential duplicates are detected. - comment: > + comment: | Hello, your issue is a duplicate of this/these issue(s): {{#issues}} - - #{{ number }} [accuracy: ({{ accuracy }}%)] + - #{{ number }} [accuracy: {{ accuracy }}%] {{/issues}} + If this is a mistake please explain why and ping @\unixfox, @\SamantazFox and @\TheFrenchGhosty. - Please refrain from opening new issues, it won't help in solving your problem. \ No newline at end of file + Please refrain from opening new issues, it won't help in solving your problem. From 372192eabc9a23373023d0ed9209059138bb4e66 Mon Sep 17 00:00:00 2001 From: Emilien Devos Date: Sun, 4 Jun 2023 17:13:48 +0200 Subject: [PATCH 220/598] warn about hmac key deadline --- src/invidious.cr | 9 +++++++-- src/invidious/views/template.ecr | 8 ++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index b5abd5c7..27c4775e 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -57,8 +57,9 @@ end # Simple alias to make code easier to read alias IV = Invidious -CONFIG = Config.load -HMAC_KEY = CONFIG.hmac_key || Random::Secure.hex(32) +CONFIG = Config.load +HMAC_KEY_CONFIGURED = CONFIG.hmac_key != nil +HMAC_KEY = CONFIG.hmac_key || Random::Secure.hex(32) PG_DB = DB.open CONFIG.database_url ARCHIVE_URL = URI.parse("https://archive.org") @@ -230,6 +231,10 @@ Kemal.config.host_binding = Kemal.config.host_binding != "0.0.0.0" ? Kemal.confi Kemal.config.port = Kemal.config.port != 3000 ? Kemal.config.port : CONFIG.port Kemal.config.app_name = "Invidious" +if !HMAC_KEY_CONFIGURED + LOGGER.warn("Please configure hmac_key by July 1st, see more here: https://github.com/iv-org/invidious/issues/3854") +end + # Use in kemal's production mode. # Users can also set the KEMAL_ENV environmental variable for this to be set automatically. {% if flag?(:release) || flag?(:production) %} diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr index 77265679..aa0fc15f 100644 --- a/src/invidious/views/template.ecr +++ b/src/invidious/views/template.ecr @@ -111,6 +111,14 @@
    <% end %> + <% if env.get? "user" %> + <% if !HMAC_KEY_CONFIGURED && CONFIG.admins.includes? env.get("user").as(Invidious::User).email %> +
    +

    Message for admin: please configure hmac_key, see more here.

    +
    + <% end %> + <% end %> + <%= content %>
    From 545a5937d87d31622e87bb2ba8151f8aecd66c81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89milien=20Devos=20=28perso=29?= Date: Tue, 6 Jun 2023 18:18:33 +0000 Subject: [PATCH 221/598] Only close at 90% similarity --- .github/workflows/auto-close-duplicate.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/auto-close-duplicate.yaml b/.github/workflows/auto-close-duplicate.yaml index aa6457ed..2eea099e 100644 --- a/.github/workflows/auto-close-duplicate.yaml +++ b/.github/workflows/auto-close-duplicate.yaml @@ -21,7 +21,7 @@ jobs: # Get issues with state to compare. Supported state: 'all', 'closed', 'open'. state: open # If similarity is higher than this threshold([0,1]), issue will be marked as duplicate. - threshold: 0.6 + threshold: 0.9 # Reactions to be add to comment when potential duplicates are detected. # Available reactions: "-1", "+1", "confused", "laugh", "heart", "hooray", "rocket", "eyes" reactions: '' From d16477602448f7f5ca0f04ffcebf3100575bf703 Mon Sep 17 00:00:00 2001 From: Chunky programmer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 6 Jun 2023 16:27:26 -0400 Subject: [PATCH 222/598] Playlists: Fix paging for Invidious playlists --- src/invidious/routes/playlists.cr | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/invidious/routes/playlists.cr b/src/invidious/routes/playlists.cr index 8675fa45..a65ff64c 100644 --- a/src/invidious/routes/playlists.cr +++ b/src/invidious/routes/playlists.cr @@ -410,8 +410,13 @@ module Invidious::Routes::Playlists return error_template(500, ex) end - page_count = (playlist.video_count / 200).to_i - page_count += 1 if (playlist.video_count % 200) > 0 + if playlist.is_a? InvidiousPlaylist + page_count = (playlist.video_count / 100).to_i + page_count += 1 if (playlist.video_count % 100) > 0 + else + page_count = (playlist.video_count / 200).to_i + page_count += 1 if (playlist.video_count % 200) > 0 + end if page > page_count return env.redirect "/playlist?list=#{plid}&page=#{page_count}" @@ -422,7 +427,11 @@ module Invidious::Routes::Playlists end begin - videos = get_playlist_videos(playlist, offset: (page - 1) * 200) + if playlist.is_a? InvidiousPlaylist + videos = get_playlist_videos(playlist, offset: (page - 1) * 100) + else + videos = get_playlist_videos(playlist, offset: (page - 1) * 200) + end rescue ex return error_template(500, "Error encountered while retrieving playlist videos.
    #{ex.message}") end From 233bd3f593c6311fb524b584a4d0da42f9ff558e Mon Sep 17 00:00:00 2001 From: Chunky programmer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Wed, 7 Jun 2023 09:55:09 -0400 Subject: [PATCH 223/598] Watch: Load watch page data for premieres --- src/invidious/videos.cr | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index 0038a97a..f38b33e5 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -394,7 +394,9 @@ def fetch_video(id, region) if reason = info["reason"]? if reason == "Video unavailable" raise NotFoundException.new(reason.as_s || "") - else + elsif !reason.as_s.starts_with? "Premieres" + # dont error when it's a premiere. + # we already parsed most of the data and display the premiere date raise InfoException.new(reason.as_s || "") end end From 45cc8356942c45cd6c94ad409279f5f36dae643a Mon Sep 17 00:00:00 2001 From: Chunky programmer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Wed, 7 Jun 2023 17:06:58 -0400 Subject: [PATCH 224/598] Comments: Don't break JavaScript when loading more --- assets/js/watch.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/watch.js b/assets/js/watch.js index cff84e4d..36506abd 100644 --- a/assets/js/watch.js +++ b/assets/js/watch.js @@ -282,7 +282,7 @@ function get_youtube_replies(target, load_more, load_replies) { if (load_more) { body = body.parentNode.parentNode; body.removeChild(body.lastElementChild); - body.innerHTML += response.contentHtml; + body.insertAdjacentHTML('beforeend', response.contentHtml); } else { body.removeChild(body.lastElementChild); From 867d488931db1b9671ab06a5e65b5439cc09c14d Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Thu, 8 Jun 2023 23:45:11 +0200 Subject: [PATCH 225/598] Makefile: Add API_ONLY variable --- Makefile | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 29be727c..929b11e1 100644 --- a/Makefile +++ b/Makefile @@ -31,6 +31,10 @@ ifeq ($(DISABLE_QUIC), 1) FLAGS += -Ddisable_quic endif +ifeq ($(API_ONLY), 1) + FLAGS += -Dapi_only +endif + # ----------------------- # Main @@ -106,11 +110,12 @@ help: @echo "" @echo "Build options available for this Makefile:" @echo "" - @echo " RELEASE Make a release build (Default: 1)" - @echo " STATIC Link libraries statically (Default: 0)" + @echo " RELEASE Make a release build (Default: 1)" + @echo " STATIC Link libraries statically (Default: 0)" @echo "" - @echo " DISABLE_QUIC Disable support for QUIC (Default: 0)" - @echo " NO_DBG_SYMBOLS Strip debug symbols (Default: 0)" + @echo " API_ONLY Build invidious without a GUI (Default: 0)" + @echo " DISABLE_QUIC Disable support for QUIC (Default: 0)" + @echo " NO_DBG_SYMBOLS Strip debug symbols (Default: 0)" From 505a1566d1769720e9d4c4a9899811f323f6f650 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Fri, 2 Jun 2023 21:02:58 +0200 Subject: [PATCH 226/598] Misc: Update User-Agent string --- src/invidious/yt_backend/connection_pool.cr | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/invidious/yt_backend/connection_pool.cr b/src/invidious/yt_backend/connection_pool.cr index 46e5bf85..b4c1878c 100644 --- a/src/invidious/yt_backend/connection_pool.cr +++ b/src/invidious/yt_backend/connection_pool.cr @@ -8,8 +8,9 @@ def add_yt_headers(request) if request.headers["User-Agent"] == "Crystal" - request.headers["User-Agent"] ||= "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36" + request.headers["User-Agent"] ||= "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" end + request.headers["Accept-Charset"] ||= "ISO-8859-1,utf-8;q=0.7,*;q=0.7" request.headers["Accept"] ||= "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" request.headers["Accept-Language"] ||= "en-us,en;q=0.5" From d9521c82cfcf4fb40323938a94d74aa552a2f517 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Fri, 2 Jun 2023 23:24:19 +0200 Subject: [PATCH 227/598] YT API: Bump iOS app version --- src/invidious/yt_backend/youtube_api.cr | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/invidious/yt_backend/youtube_api.cr b/src/invidious/yt_backend/youtube_api.cr index 91a9332c..a1a54d60 100644 --- a/src/invidious/yt_backend/youtube_api.cr +++ b/src/invidious/yt_backend/youtube_api.cr @@ -12,11 +12,13 @@ module YoutubeAPI private ANDROID_USER_AGENT = "com.google.android.youtube/17.33.42 (Linux; U; Android 12; US) gzip" private ANDROID_SDK_VERSION = 31_i64 private ANDROID_VERSION = "12" - private IOS_APP_VERSION = "17.33.2" + + private IOS_APP_VERSION = "18.21.3" # github.com/TeamNewPipe/NewPipeExtractor/blob/943b7c033bb9d07ead63ddab4441c287653e4384/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java#L1330 - private IOS_USER_AGENT = "com.google.ios.youtube/17.33.2 (iPhone14,5; U; CPU iOS 15_6 like Mac OS X;)" + private IOS_USER_AGENT = "com.google.ios.youtube/18.21.3 (iPhone14,5; U; CPU iOS 15_6 like Mac OS X;)" # github.com/TeamNewPipe/NewPipeExtractor/blob/943b7c033bb9d07ead63ddab4441c287653e4384/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java#L1224 - private IOS_VERSION = "15.6.0.19G71" + private IOS_VERSION = "15.6.0.19G71" + private WINDOWS_VERSION = "10.0" # Enumerate used to select one of the clients supported by the API From b5e30d66d4134731e01c2ceb1ef3f6f91dce1c0b Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Fri, 2 Jun 2023 23:25:28 +0200 Subject: [PATCH 228/598] YT API: Bump Android app version --- src/invidious/yt_backend/youtube_api.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/invidious/yt_backend/youtube_api.cr b/src/invidious/yt_backend/youtube_api.cr index a1a54d60..399880c7 100644 --- a/src/invidious/yt_backend/youtube_api.cr +++ b/src/invidious/yt_backend/youtube_api.cr @@ -7,9 +7,9 @@ module YoutubeAPI private DEFAULT_API_KEY = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8" - private ANDROID_APP_VERSION = "17.33.42" + private ANDROID_APP_VERSION = "18.20.38" # github.com/TeamNewPipe/NewPipeExtractor/blob/943b7c033bb9d07ead63ddab4441c287653e4384/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java#L1308 - private ANDROID_USER_AGENT = "com.google.android.youtube/17.33.42 (Linux; U; Android 12; US) gzip" + private ANDROID_USER_AGENT = "com.google.android.youtube/18.20.38 (Linux; U; Android 12; US) gzip" private ANDROID_SDK_VERSION = 31_i64 private ANDROID_VERSION = "12" From 7556cb69f256dc595a889d20387071cf0659aee0 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Fri, 2 Jun 2023 23:37:46 +0200 Subject: [PATCH 229/598] YT API: Bump WEB/MWEB client versions --- src/invidious/yt_backend/youtube_api.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/invidious/yt_backend/youtube_api.cr b/src/invidious/yt_backend/youtube_api.cr index 399880c7..3dd9e9d8 100644 --- a/src/invidious/yt_backend/youtube_api.cr +++ b/src/invidious/yt_backend/youtube_api.cr @@ -45,7 +45,7 @@ module YoutubeAPI ClientType::Web => { name: "WEB", name_proto: "1", - version: "2.20221118.01.00", + version: "2.20230602.01.00", api_key: DEFAULT_API_KEY, screen: "WATCH_FULL_SCREEN", os_name: "Windows", @@ -65,7 +65,7 @@ module YoutubeAPI ClientType::WebMobile => { name: "MWEB", name_proto: "2", - version: "2.20220805.01.00", + version: "2.20230531.05.00", api_key: DEFAULT_API_KEY, os_name: "Android", os_version: ANDROID_VERSION, From 1b942f4f0a9b9bad3b9447de2adb99401204cc2c Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 10 Jun 2023 20:57:36 +0200 Subject: [PATCH 230/598] User: Strip empty new lines before parsing CSV --- src/invidious/user/imports.cr | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/invidious/user/imports.cr b/src/invidious/user/imports.cr index e4b25156..0a2fe1e2 100644 --- a/src/invidious/user/imports.cr +++ b/src/invidious/user/imports.cr @@ -6,7 +6,7 @@ struct Invidious::User # Parse a youtube CSV subscription file def parse_subscription_export_csv(csv_content : String) - rows = CSV.new(csv_content, headers: true) + rows = CSV.new(csv_content.strip('\n'), headers: true) subscriptions = Array(String).new # Counter to limit the amount of imports. @@ -32,10 +32,10 @@ struct Invidious::User def parse_playlist_export_csv(user : User, raw_input : String) # Split the input into head and body content - raw_head, raw_body = raw_input.split("\n\n", limit: 2, remove_empty: true) + raw_head, raw_body = raw_input.strip('\n').split("\n\n", limit: 2, remove_empty: true) # Create the playlist from the head content - csv_head = CSV.new(raw_head, headers: true) + csv_head = CSV.new(raw_head.strip('\n'), headers: true) csv_head.next title = csv_head[4] description = csv_head[5] @@ -51,7 +51,7 @@ struct Invidious::User Invidious::Database::Playlists.update_description(playlist.id, description) # Add each video to the playlist from the body content - csv_body = CSV.new(raw_body, headers: true) + csv_body = CSV.new(raw_body.strip('\n'), headers: true) csv_body.each do |row| video_id = row[0] if playlist From 281c8ecbf5fd09e76126f8fe09558da354b4f707 Mon Sep 17 00:00:00 2001 From: IceTheDev2 <115871297+IceTheDev2@users.noreply.github.com> Date: Sun, 11 Jun 2023 14:26:18 +0300 Subject: [PATCH 231/598] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 602ad2e2..88770383 100644 --- a/README.md +++ b/README.md @@ -153,9 +153,9 @@ Weblate also allows you to log-in with major SSO providers like Github, Gitlab, - [WatchTube](https://github.com/WatchTubeTeam/WatchTube): Powerful YouTube client for Apple Watch. - [Yattee](https://github.com/yattee/yattee): Alternative YouTube frontend for iPhone, iPad, Mac and Apple TV. - [TubiTui](https://codeberg.org/777/TubiTui): A lightweight, libre, TUI-based YouTube client. -- [Ytfzf](https://github.com/pystardust/ytfzf): A posix script to find and watch youtube videos from the terminal. (Without API) -- [Playlet](https://github.com/iBicha/playlet): Unofficial Youtube client for Roku TV -- [Clipious](https://github.com/lamarios/clipious): Unofficial Invidious client for Android +- [Ytfzf](https://github.com/pystardust/ytfzf): A posix script to find and watch youtube videos from the terminal. (Without API). +- [Playlet](https://github.com/iBicha/playlet): Unofficial Youtube client for Roku TV. +- [Clipious](https://github.com/lamarios/clipious): Unofficial Invidious client for Android. ## Liability From fda8d2d4d3ceec27d9f13030f119ee6f287325ba Mon Sep 17 00:00:00 2001 From: Andrey Date: Sat, 27 May 2023 14:48:37 +0000 Subject: [PATCH 232/598] Update Russian translation --- locales/ru.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/ru.json b/locales/ru.json index 0031f79a..f5b5211d 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -499,5 +499,6 @@ "Song: ": "Композиция: ", "Standard YouTube license": "Стандартная лицензия YouTube", "Channel Sponsor": "Спонсор канала", - "Download is disabled": "Загрузка отключена" + "Download is disabled": "Загрузка отключена", + "Import YouTube playlist (.csv)": "Импорт плейлиста YouTube (.csv)" } From 14a5751a47758286310036741e97de9f5b19c541 Mon Sep 17 00:00:00 2001 From: gallegonovato Date: Sat, 27 May 2023 11:11:11 +0000 Subject: [PATCH 233/598] Update Spanish translation --- locales/es.json | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/locales/es.json b/locales/es.json index 0425ed68..f3942b48 100644 --- a/locales/es.json +++ b/locales/es.json @@ -398,12 +398,15 @@ "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", + "generic_views_count_0": "{{count}} visualización", + "generic_views_count_1": "{{count}} visualizaciones", + "generic_views_count_2": "{{count}} visualizaciones", + "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": "{{count}} notificación no vista", "subscriptions_unseen_notifs_count_plural": "{{count}} notificaciones no vistas", "generic_count_days": "{{count}} día", @@ -412,10 +415,12 @@ "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_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}} video", + "generic_videos_count_2": "{{count}} vídeos", "generic_count_months": "{{count}} mes", "generic_count_months_plural": "{{count}} meses", "comments_points_count": "{{count}} punto", @@ -468,8 +473,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 fd3e2aa8686469831938213e4c7c232f0c79e0c8 Mon Sep 17 00:00:00 2001 From: maboroshin Date: Sat, 27 May 2023 14:20:22 +0000 Subject: [PATCH 234/598] Update Japanese translation --- locales/ja.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/locales/ja.json b/locales/ja.json index d9207d3f..114eaa5c 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -1,9 +1,9 @@ { "generic_views_count_0": "{{count}} 回視聴", - "generic_videos_count_0": "{{count}} 個の動画", - "generic_playlists_count_0": "{{count}} 個の再生リスト", + "generic_videos_count_0": "{{count}}本の動画", + "generic_playlists_count_0": "{{count}}個の再生リスト", "generic_subscribers_count_0": "{{count}} 人の登録者", - "generic_subscriptions_count_0": "{{count}} 個の登録チャンネル", + "generic_subscriptions_count_0": "{{count}}個の登録チャンネル", "LIVE": "ライブ", "Shared `x` ago": "`x`前に公開", "Unsubscribe": "登録解除", @@ -56,11 +56,11 @@ "preferences_category_player": "プレイヤーの設定", "preferences_video_loop_label": "常にループ: ", "preferences_autoplay_label": "自動再生: ", - "preferences_continue_label": "次の動画を再生: ", + "preferences_continue_label": "次の動画を再生をオン: ", "preferences_continue_autoplay_label": "次の動画を自動再生: ", - "preferences_listen_label": "デフォルトで音声モードを使用: ", + "preferences_listen_label": "音声モードを使用: ", "preferences_local_label": "動画視聴にプロキシーを経由: ", - "preferences_speed_label": "標準の再生速度: ", + "preferences_speed_label": "再生速度の初期値: ", "preferences_quality_label": "優先する画質: ", "preferences_volume_label": "プレイヤーの音量: ", "preferences_comments_label": "デフォルトのコメント: ", @@ -120,12 +120,12 @@ "Subscription manager": "登録チャンネルの管理", "Token manager": "トークンの管理", "Token": "トークン", - "tokens_count_0": "{{count}} 個のトークン", + "tokens_count_0": "{{count}}個のトークン", "Import/export": "インポート/エクスポート", "unsubscribe": "登録解除", "revoke": "取り消す", "Subscriptions": "登録チャンネル", - "subscriptions_unseen_notifs_count_0": "{{count}} 個の未読通知", + "subscriptions_unseen_notifs_count_0": "{{count}}件の未読通知", "search": "検索", "Log out": "ログアウト", "Released under the AGPLv3 on Github.": "GitHub 上で AGPLv3 の元で公開", @@ -444,7 +444,7 @@ "Popular enabled: ": "人気動画を有効化 ", "search_message_use_another_instance": " 別のインスタンス上での検索も可能です。", "search_filters_apply_button": "選択したフィルターを適用", - "user_saved_playlists": "`x` 個の保存した再生リスト", + "user_saved_playlists": "`x`個の保存済みの再生リスト", "crash_page_you_found_a_bug": "Invidious のバグのようです!", "crash_page_refresh": "ページを更新を試す", "preferences_watch_history_label": "再生履歴を有効化 ", From 3b6474d72b36d522945141e450f35a4e43440723 Mon Sep 17 00:00:00 2001 From: xrfmkrh Date: Sat, 27 May 2023 07:01:48 +0000 Subject: [PATCH 235/598] Update Korean translation --- locales/ko.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/ko.json b/locales/ko.json index 2b454add..15357ae4 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -467,5 +467,6 @@ "Album: ": "앨범: ", "Music in this video": "동영상 속 음악", "Artist: ": "아티스트: ", - "Download is disabled": "다운로드가 비활성화 되어있음" + "Download is disabled": "다운로드가 비활성화 되어있음", + "Import YouTube playlist (.csv)": "유튜브 플레이리스트 가져오기 (.csv)" } From 3690631cdd3bc9fb8783d611cd9f87e324de6b1b Mon Sep 17 00:00:00 2001 From: joaooliva Date: Thu, 1 Jun 2023 18:31:28 +0000 Subject: [PATCH 236/598] Update Portuguese (Brazil) translation --- locales/pt-BR.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/pt-BR.json b/locales/pt-BR.json index 759aec94..0a33e380 100644 --- a/locales/pt-BR.json +++ b/locales/pt-BR.json @@ -483,5 +483,6 @@ "Standard YouTube license": "Licença padrão do YouTube", "Song: ": "Música: ", "Channel Sponsor": "Patrocinador do Canal", - "Download is disabled": "Download está desativado" + "Download is disabled": "Download está desativado", + "Import YouTube playlist (.csv)": "Importar lista de reprodução do YouTube (.csv)" } From d250b4132bb469d62c8c3da8c3d25e3d3952e4ee Mon Sep 17 00:00:00 2001 From: 04f7rx0n6 <04f7rx0n6@proton.me> Date: Fri, 2 Jun 2023 15:05:50 +0000 Subject: [PATCH 237/598] Update Russian translation --- locales/ru.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/ru.json b/locales/ru.json index f5b5211d..5907d567 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -491,7 +491,7 @@ "error_video_not_in_playlist": "Запрошенного видео нет в этой подборке. Нажмите тут, чтобы вернуться к странице подборки.", "channel_tab_playlists_label": "Подборки", "channel_tab_channels_label": "Каналы", - "channel_tab_streams_label": "Живое вещание", + "channel_tab_streams_label": "Стримы", "channel_tab_shorts_label": "Shorts", "Music in this video": "Музыка в этом видео", "Artist: ": "Исполнитель: ", From daccbc2abb202d9542c5f11e4eb74a17c8a28dbd Mon Sep 17 00:00:00 2001 From: Translator Date: Sun, 4 Jun 2023 09:36:18 +0000 Subject: [PATCH 238/598] Update French translation --- locales/fr.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index bb40916b..29895703 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -473,7 +473,7 @@ "search_filters_features_option_vr180": "VR180", "search_filters_duration_option_none": "Toutes les durées", "error_video_not_in_playlist": "La vidéo demandée n'existe pas dans cette liste de lecture. Cliquez ici pour retourner à la liste de lecture.", - "channel_tab_shorts_label": "Clips", + "channel_tab_shorts_label": "Vidéos courtes", "channel_tab_streams_label": "Vidéos en direct", "channel_tab_playlists_label": "Listes de lecture", "channel_tab_channels_label": "Chaînes", @@ -483,5 +483,6 @@ "Standard YouTube license": "Licence YouTube Standard", "Music in this video": "Musique dans cette vidéo", "Channel Sponsor": "Soutien de la chaîne", - "Download is disabled": "Le téléchargement est désactivé" + "Download is disabled": "Le téléchargement est désactivé", + "Import YouTube playlist (.csv)": "Importer des listes de lecture de Youtube (.csv)" } From 50d6a2afb9c75a63bb13ed5e0187a5f56c98ed3f Mon Sep 17 00:00:00 2001 From: Nicolas Dommanget-Muller Date: Sun, 4 Jun 2023 10:08:06 +0000 Subject: [PATCH 239/598] Update French translation --- locales/fr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index 29895703..688267b5 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -478,7 +478,7 @@ "channel_tab_playlists_label": "Listes de lecture", "channel_tab_channels_label": "Chaînes", "Song: ": "Chanson : ", - "Artist: ": "Artiste : ", + "Artist: ": "Artiste: ", "Album: ": "Album : ", "Standard YouTube license": "Licence YouTube Standard", "Music in this video": "Musique dans cette vidéo", From 37bab74085573f205734c98fbf6130198ab22787 Mon Sep 17 00:00:00 2001 From: maboroshin Date: Sun, 4 Jun 2023 08:33:36 +0000 Subject: [PATCH 240/598] Update Japanese translation --- locales/ja.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/locales/ja.json b/locales/ja.json index 114eaa5c..700d6f4f 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -109,7 +109,7 @@ "Delete account": "アカウントを削除", "preferences_category_admin": "管理者設定", "preferences_default_home_label": "ホームに表示するページ: ", - "preferences_feed_menu_label": "フィードメニュー: ", + "preferences_feed_menu_label": "フィードのメニュー: ", "preferences_show_nick_label": "ログイン名を上部に表示: ", "Top enabled: ": "トップページを有効化: ", "CAPTCHA enabled: ": "CAPTCHA を有効化: ", @@ -347,7 +347,7 @@ "search_filters_sort_option_relevance": "関連度", "search_filters_sort_option_rating": "評価", "search_filters_sort_option_date": "アップロード日", - "search_filters_sort_option_views": "再生回数", + "search_filters_sort_option_views": "視聴回数", "search_filters_type_label": "種類", "search_filters_duration_label": "再生時間", "search_filters_features_label": "特徴", @@ -383,7 +383,7 @@ "search_filters_duration_option_long": "20 分以上", "preferences_region_label": "地域: ", "footer_donate_page": "寄付する", - "preferences_quality_dash_label": "優先するDash画質 : ", + "preferences_quality_dash_label": "優先するDASH画質: ", "preferences_quality_dash_option_4320p": "4320p", "preferences_quality_dash_option_240p": "240p", "preferences_quality_dash_option_144p": "144p", From a4ca460651e68905d1db24e0ad0dbde627c056a3 Mon Sep 17 00:00:00 2001 From: Translator Date: Sun, 4 Jun 2023 10:08:59 +0000 Subject: [PATCH 241/598] Update French translation --- locales/fr.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index 688267b5..29895703 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -478,7 +478,7 @@ "channel_tab_playlists_label": "Listes de lecture", "channel_tab_channels_label": "Chaînes", "Song: ": "Chanson : ", - "Artist: ": "Artiste: ", + "Artist: ": "Artiste : ", "Album: ": "Album : ", "Standard YouTube license": "Licence YouTube Standard", "Music in this video": "Musique dans cette vidéo", From f954483eac2be7c289d43bfa6c24d315b413b73d Mon Sep 17 00:00:00 2001 From: maboroshin Date: Sun, 4 Jun 2023 11:21:09 +0000 Subject: [PATCH 242/598] Update Japanese translation --- locales/ja.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/ja.json b/locales/ja.json index 700d6f4f..157862c6 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -59,7 +59,7 @@ "preferences_continue_label": "次の動画を再生をオン: ", "preferences_continue_autoplay_label": "次の動画を自動再生: ", "preferences_listen_label": "音声モードを使用: ", - "preferences_local_label": "動画視聴にプロキシーを経由: ", + "preferences_local_label": "動画視聴にプロキシを経由: ", "preferences_speed_label": "再生速度の初期値: ", "preferences_quality_label": "優先する画質: ", "preferences_volume_label": "プレイヤーの音量: ", @@ -403,7 +403,7 @@ "none": "なし", "download_subtitles": "字幕 - `x` (.vtt)", "search_filters_features_option_purchased": "購入済み", - "preferences_quality_option_dash": "DASH (適応品質)", + "preferences_quality_option_dash": "DASH (適応的画質)", "preferences_quality_dash_option_worst": "最悪", "preferences_quality_dash_option_best": "最高", "videoinfo_started_streaming_x_ago": "`x`前に配信を開始", From 52c317f2357b1e6aef774eb6bcbc1f0ff476e707 Mon Sep 17 00:00:00 2001 From: Daniele Tricoli Date: Sat, 10 Jun 2023 16:20:20 +0000 Subject: [PATCH 243/598] Update Italian translation --- locales/it.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/locales/it.json b/locales/it.json index 9299add7..1825ae70 100644 --- a/locales/it.json +++ b/locales/it.json @@ -154,8 +154,9 @@ "Whitelisted regions: ": "Regioni in lista bianca: ", "Blacklisted regions: ": "Regioni in lista nera: ", "Shared `x`": "Condiviso `x`", - "generic_views_count": "{{count}} visualizzazione", - "generic_views_count_plural": "{{count}} visualizzazioni", + "generic_views_count_0": "{{count}} visualizzazione", + "generic_views_count_1": "{{count}} visualizzazioni", + "generic_views_count_2": "{{count}} visualizzazioni", "Premieres in `x`": "In anteprima in `x`", "Premieres `x`": "In anteprima `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.": "Ciao, Sembra che tu abbia disattivato JavaScript. Clicca qui per visualizzare i commenti, ma considera che il caricamento potrebbe richiedere più tempo.", From 96238d719d30e35e1725cd6b2ee35896cbcb390c Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sun, 11 Jun 2023 16:19:05 +0200 Subject: [PATCH 244/598] Fix broken Spanish locale (i18next v3->v4 mixup) --- locales/es.json | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/locales/es.json b/locales/es.json index f3942b48..76145a67 100644 --- a/locales/es.json +++ b/locales/es.json @@ -398,15 +398,12 @@ "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}} visualización", - "generic_views_count_1": "{{count}} visualizaciones", - "generic_views_count_2": "{{count}} visualizaciones", - "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", + "generic_views_count": "{{count}} visualización", + "generic_views_count_plural": "{{count}} visualizaciones", + "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", @@ -415,12 +412,10 @@ "comments_view_x_replies_plural": "Ver {{count}} respuestas", "generic_count_weeks": "{{count}} semana", "generic_count_weeks_plural": "{{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}} video", - "generic_videos_count_2": "{{count}} vídeos", + "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}} video", "generic_count_months": "{{count}} mes", "generic_count_months_plural": "{{count}} meses", "comments_points_count": "{{count}} punto", @@ -473,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 5af87f97a38c81033333ae636a9a71ac4700d5f7 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sun, 11 Jun 2023 16:31:47 +0200 Subject: [PATCH 245/598] Fix broken Italian locale (i18next v3->v4 mixup) --- locales/it.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/locales/it.json b/locales/it.json index 1825ae70..9299add7 100644 --- a/locales/it.json +++ b/locales/it.json @@ -154,9 +154,8 @@ "Whitelisted regions: ": "Regioni in lista bianca: ", "Blacklisted regions: ": "Regioni in lista nera: ", "Shared `x`": "Condiviso `x`", - "generic_views_count_0": "{{count}} visualizzazione", - "generic_views_count_1": "{{count}} visualizzazioni", - "generic_views_count_2": "{{count}} visualizzazioni", + "generic_views_count": "{{count}} visualizzazione", + "generic_views_count_plural": "{{count}} visualizzazioni", "Premieres in `x`": "In anteprima in `x`", "Premieres `x`": "In anteprima `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.": "Ciao, Sembra che tu abbia disattivato JavaScript. Clicca qui per visualizzare i commenti, ma considera che il caricamento potrebbe richiedere più tempo.", From 8d2ab70cbc80ba83baf6e94cf10b8f9d66519b3e Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 10 Jun 2023 17:57:28 +0200 Subject: [PATCH 246/598] User: Remove broken Google login (localized strings) --- locales/ar.json | 8 -------- locales/bn.json | 3 --- locales/bn_BD.json | 3 --- locales/ca.json | 8 -------- locales/cs.json | 8 -------- locales/da.json | 8 -------- locales/de.json | 8 -------- locales/el.json | 8 -------- locales/en-US.json | 8 -------- locales/eo.json | 8 -------- locales/es.json | 8 -------- locales/et.json | 6 ------ locales/eu.json | 8 -------- locales/fa.json | 8 -------- locales/fi.json | 8 -------- locales/fr.json | 8 -------- locales/he.json | 4 ---- locales/hi.json | 8 -------- locales/hr.json | 8 -------- locales/hu-HU.json | 8 -------- locales/id.json | 8 -------- locales/is.json | 8 -------- locales/it.json | 8 -------- locales/ja.json | 8 -------- locales/ko.json | 8 -------- locales/lt.json | 8 -------- locales/nb-NO.json | 8 -------- locales/nl.json | 8 -------- locales/pl.json | 8 -------- locales/pt-BR.json | 8 -------- locales/pt-PT.json | 8 -------- locales/pt.json | 8 -------- locales/ro.json | 8 -------- locales/ru.json | 8 -------- locales/si.json | 3 --- locales/sk.json | 3 --- locales/sl.json | 8 -------- locales/sq.json | 8 -------- locales/sr.json | 8 -------- locales/sr_Cyrl.json | 8 -------- locales/sv-SE.json | 8 -------- locales/tr.json | 8 -------- locales/uk.json | 8 -------- locales/vi.json | 8 -------- locales/zh-CN.json | 8 -------- locales/zh-TW.json | 8 -------- 46 files changed, 342 deletions(-) diff --git a/locales/ar.json b/locales/ar.json index 6fe5b8bf..2e275e77 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -14,7 +14,6 @@ "Clear watch history?": "هل تريد محو سجل المشاهدة؟", "New password": "كلمة مرور جديدة", "New passwords must match": "يَجبُ أن تكون كلمتا المرور متطابقتين", - "Cannot change password for Google accounts": "لا يُمكن تغيير كلمة المرور لِحسابات جوجل", "Authorize token?": "رمز التفويض؟", "Authorize token for `x`?": "السماح بالرمز المميز ل 'x'؟", "Yes": "نعم", @@ -37,7 +36,6 @@ "source": "المصدر", "Log in": "تسجيل الدخول", "Log in/register": "تسجيل الدخول \\ إنشاء حساب", - "Log in with Google": "تسجيل الدخول باستخدام جوجل", "User ID": "مُعرِّف المُستخدم", "Password": "كلمة المرور", "Time (h:mm:ss):": "الوقت (h:mm:ss):", @@ -46,7 +44,6 @@ "Sign In": "تسجيل الدخول", "Register": "التسجيل", "E-mail": "البريد الإلكتروني", - "Google verification code": "رمز تحقق جوجل", "Preferences": "الإعدادات", "preferences_category_player": "إعدادات المُشغِّل", "preferences_video_loop_label": "كرر المقطع المرئيّ دائما: ", @@ -164,17 +161,12 @@ "Hide replies": "إخفاء الردود", "Show replies": "عرض الردود", "Incorrect password": "كلمة السر غير صحيحة", - "Quota exceeded, try again in a few hours": "تم تجاوز عدد المرات المسموح بها، حاول مجددًا بعد بضع ساعات", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "غير قادر على تسجيل الدخول، تأكد من تشغيل المصادقة الثنائية 2FA.", - "Invalid TFA code": "كود مصادقة ثنائية 2FA غير صحيح", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "فشل تسجيل الدخول. قد يكون هذا بسبب أن المصادقة الثنائية 2FA معطلة في حسابك.", "Wrong answer": "إجابة خاطئة", "Erroneous CAPTCHA": "الكابتشا CAPTCHA غير صاحلة", "CAPTCHA is a required field": "مكان الكابتشا CAPTCHA مطلوب", "User ID is a required field": "مكان اسم المستخدم مطلوب", "Password is a required field": "مكان كلمة السر مطلوب", "Wrong username or password": "اسم المستخدم او كلمة السر غير صحيح", - "Please sign in using 'Log in with Google'": "الرجاء تسجيل الدخول باستخدام \"تسجيل الدخول باستخدام Google\"", "Password cannot be empty": "لا يمكن أن تكون كلمة السر فارغة", "Password cannot be longer than 55 characters": "يجب أن لا تتعدى كلمة السر 55 حرفًا", "Please log in": "الرجاء تسجيل الدخول", diff --git a/locales/bn.json b/locales/bn.json index 3d1cb5da..9d1c7b24 100644 --- a/locales/bn.json +++ b/locales/bn.json @@ -11,7 +11,6 @@ "Clear watch history?": "দেখার ইতিহাস সাফ করবেন?", "New password": "নতুন পাসওয়ার্ড", "New passwords must match": "নতুন পাসওয়ার্ড অবশ্যই মিলতে হবে", - "Cannot change password for Google accounts": "গুগল অ্যাকাউন্টগুলোর জন্য পাসওয়ার্ড পরিবর্তন করা যায় না", "Authorize token?": "টোকেন অনুমোদন করবেন?", "Authorize token for `x`?": "`x` -এর জন্য টোকেন অনুমোদন?", "Yes": "হ্যাঁ", @@ -34,7 +33,6 @@ "source": "সূত্র", "Log in": "লগ ইন", "Log in/register": "লগ ইন/রেজিস্টার", - "Log in with Google": "গুগল দিয়ে লগ ইন করুন", "User ID": "ইউজার আইডি", "Password": "পাসওয়ার্ড", "Time (h:mm:ss):": "সময় (ঘণ্টা:মিনিট:সেকেন্ড):", @@ -43,7 +41,6 @@ "Sign In": "সাইন ইন", "Register": "নিবন্ধন", "E-mail": "ই-মেইল", - "Google verification code": "গুগল যাচাইকরণ কোড", "Preferences": "পছন্দসমূহ", "preferences_category_player": "প্লেয়ারের পছন্দসমূহ", "preferences_video_loop_label": "সর্বদা লুপ: ", diff --git a/locales/bn_BD.json b/locales/bn_BD.json index 53cb79ae..a82b0da7 100644 --- a/locales/bn_BD.json +++ b/locales/bn_BD.json @@ -14,7 +14,6 @@ "Clear watch history?": "দেখার ইতিহাস সাফ করবেন?", "New password": "নতুন পাসওয়ার্ড", "New passwords must match": "নতুন পাসওয়ার্ড অবশ্যই মিলতে হবে", - "Cannot change password for Google accounts": "গুগল অ্যাকাউন্টগুলোর জন্য পাসওয়ার্ড পরিবর্তন করা যায় না", "Authorize token?": "টোকেন অনুমোদন করবেন?", "Authorize token for `x`?": "`x` -এর জন্য টোকেন অনুমোদন?", "Yes": "হ্যাঁ", @@ -37,7 +36,6 @@ "source": "সূত্র", "Log in": "লগ ইন", "Log in/register": "লগ ইন/রেজিস্টার", - "Log in with Google": "গুগল দিয়ে লগ ইন করুন", "User ID": "ইউজার আইডি", "Password": "পাসওয়ার্ড", "Time (h:mm:ss):": "সময় (ঘণ্টা:মিনিট:সেকেন্ড):", @@ -46,7 +44,6 @@ "Sign In": "সাইন ইন", "Register": "নিবন্ধন", "E-mail": "ই-মেইল", - "Google verification code": "গুগল যাচাইকরণ কোড", "Preferences": "পছন্দসমূহ", "preferences_category_player": "প্লেয়ারের পছন্দসমূহ", "preferences_video_loop_label": "সর্বদা লুপ: ", diff --git a/locales/ca.json b/locales/ca.json index 901249ac..6a320b02 100644 --- a/locales/ca.json +++ b/locales/ca.json @@ -4,7 +4,6 @@ "preferences_quality_label": "Qualitat de vídeo preferida: ", "newest": "més nou", "No": "No", - "Google verification code": "Codi de verificació de Google", "User ID": "ID d'usuari", "Preferences": "Preferències", "Dark mode: ": "Mode fosc: ", @@ -137,7 +136,6 @@ "channel_tab_channels_label": "Canals", "channel_tab_playlists_label": "Llistes de reproducció", "channel_tab_community_label": "Comunitat", - "Invalid TFA code": "Codi TFA no vàlid", "Czech": "Txec", "Default": "Per defecte", "Amharic": "Amàric", @@ -186,7 +184,6 @@ "Released under the AGPLv3 on Github.": "Publicat sota l'AGPLv3 a GitHub.", "Token manager": "Gestor de testimonis", "Watch history": "Historial de reproduccions", - "Cannot change password for Google accounts": "No es pot canviar la contrasenya dels comptes de Google", "Authorize token?": "Autoritzar testimoni?", "Source available here.": "Font disponible aquí.", "Export subscriptions as OPML (for NewPipe & FreeTube)": "Exporta subscripcions com a OPML (per a NewPipe i FreeTube)", @@ -225,7 +222,6 @@ }, "View Reddit comments": "Veure comentaris de Reddit", "Incorrect password": "Contrasenya incorrecta", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "No es pot iniciar la sessió, assegureu-vos que l'autenticació de dos factors (Autenticador o SMS) estigui activada.", "Erroneous CAPTCHA": "CAPTCHA erroni", "CAPTCHA is a required field": "El CAPTCHA és un camp obligatori", "Korean (auto-generated)": "Coreà (generat automàticament)", @@ -272,7 +268,6 @@ "Khmer": "Khmer", "This channel does not exist.": "Aquest canal no existeix.", "Song: ": "Cançó: ", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "S'ha produït un error en iniciar sessió. Això pot ser perquè l'autenticació de dos factors no està activada per al vostre compte.", "channel:`x`": "canal: `x`", "Deleted or invalid channel": "Canal suprimit o no vàlid", "Could not get channel info.": "No s'ha pogut obtenir la informació del canal.", @@ -291,7 +286,6 @@ "User ID is a required field": "L'identificador d'usuari és un camp obligatori", "Password is a required field": "La contrasenya és un camp obligatori", "Wrong username or password": "Nom d'usuari o contrasenya incorrectes", - "Please sign in using 'Log in with Google'": "Si us plau, inicieu la sessió amb 'Inicieu sessió amb Google'", "Password cannot be longer than 55 characters": "La contrasenya no pot tenir més de 55 caràcters", "Invidious Private Feed for `x`": "Feed privat Invidious per a `x`", "generic_views_count": "{{count}} visualització", @@ -436,7 +430,6 @@ "preferences_quality_dash_option_240p": "240p", "preferences_quality_dash_option_720p": "720p", "preferences_quality_dash_option_480p": "480p", - "Log in with Google": "Inicia sessió amb Google", "preferences_quality_dash_option_1440p": "1440p", "Previous page": "Pàgina anterior", "Only show latest video from channel: ": "Mostra només l'últim vídeo del canal: ", @@ -445,7 +438,6 @@ "Import NewPipe subscriptions (.json)": "Importar subscripcions de NewPipe (.json)", "crash_page_you_found_a_bug": "Heu trobat un error a Invidious!", "Subscribe": "Subscriu-me", - "Quota exceeded, try again in a few hours": "S'ha superat la quota, torna-ho a provar d'aquí a unes hores", "generic_count_days": "{{count}} dia", "generic_count_days_plural": "{{count}} dies", "Trending": "Tendència", diff --git a/locales/cs.json b/locales/cs.json index 8e656827..73ed960d 100644 --- a/locales/cs.json +++ b/locales/cs.json @@ -14,7 +14,6 @@ "Clear watch history?": "Smazat historii?", "New password": "Nové heslo", "New passwords must match": "Hesla se musí shodovat", - "Cannot change password for Google accounts": "Nelze změnit heslo pro účty Google", "Authorize token?": "Autorizovat token?", "Authorize token for `x`?": "Autorizovat token pro `x`?", "Yes": "Ano", @@ -37,7 +36,6 @@ "source": "zdrojový kód", "Log in": "Přihlásit se", "Log in/register": "Přihlásit se/vytvořit účet", - "Log in with Google": "Přihlásit se s Googlem", "User ID": "ID uživatele", "Password": "Heslo", "Time (h:mm:ss):": "Čas (h:mm:ss):", @@ -46,7 +44,6 @@ "Sign In": "Přihlásit se", "Register": "Vytvořit účet", "E-mail": "E-mail", - "Google verification code": "Verifikační číslo Google", "Preferences": "Nastavení", "preferences_category_player": "Nastavení přehravače", "preferences_video_loop_label": "Vždy opakovat: ", @@ -335,7 +332,6 @@ "preferences_quality_dash_option_1440p": "1440p", "invidious": "Invidious", "View more comments on Reddit": "Zobrazit více komentářů na Redditu", - "Invalid TFA code": "Nesprávný TFA kód", "generic_playlists_count_0": "{{count}} playlist", "generic_playlists_count_1": "{{count}} playlisty", "generic_playlists_count_2": "{{count}} playlistů", @@ -349,7 +345,6 @@ "subscriptions_unseen_notifs_count_1": "{{count}} nezobrazená oznámení", "subscriptions_unseen_notifs_count_2": "{{count}} nezobrazených oznámení", "Show replies": "Zobrazit odpovědi", - "Quota exceeded, try again in a few hours": "Kvóta překročena, zkuste to znovu za pár hodin", "Password cannot be longer than 55 characters": "Heslo nesmí být delší než 55 znaků", "comments_view_x_replies_0": "Zobrazit {{count}} odpověď", "comments_view_x_replies_1": "Zobrazit {{count}} odpovědi", @@ -433,7 +428,6 @@ "View YouTube comments": "Zobrazit YouTube komentáře", "Blacklisted regions: ": "Oblasti na černé listině: ", "Wrong username or password": "Nesprávné uživatelské jméno nebo heslo", - "Please sign in using 'Log in with Google'": "Přihlaste se prosím pomocí Googlu", "Password cannot be empty": "Heslo nemůže být prázné", "preferences_category_misc": "Různá nastavení", "preferences_show_nick_label": "Zobrazit přezdívku na vrchu: ", @@ -452,8 +446,6 @@ "([^.,0-9]|^)1([^.,0-9]|$)": "Zobrazit `x` komentář", "": "Zobrazit `x` komentářů" }, - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Nepodařilo se přihlásit, ujistěte se, že je povoleno dvoufázové ověřování (autentifikátor nebo SMS).", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "Přihlášení selhalo. Toto se může stát, když není na vašem účtu povolené dvoufázové ověřování.", "Could not get channel info.": "Nepodařilo se získat informace o kanálu.", "Could not fetch comments": "Nepodařilo se získat komentáře", "Could not create mix.": "Nepodařilo se vytvořit mix.", diff --git a/locales/da.json b/locales/da.json index 2bee6c80..16607546 100644 --- a/locales/da.json +++ b/locales/da.json @@ -14,7 +14,6 @@ "Clear watch history?": "Ryd afspilningshistorik?", "New password": "Nyt kodeord", "New passwords must match": "Nye kodeord skal matche", - "Cannot change password for Google accounts": "Kan ikke skifte kodeord til Google-konti", "Authorize token?": "Godkend token?", "Authorize token for `x`?": "Godkend token til `x`?", "Yes": "Ja", @@ -37,7 +36,6 @@ "source": "kilde", "Log in": "Log på", "Log in/register": "Log på/registrer", - "Log in with Google": "Log på med Google", "User ID": "Bruger ID", "Password": "Kodeord", "Time (h:mm:ss):": "Tid (t:mm:ss):", @@ -46,7 +44,6 @@ "Sign In": "Log ind", "Register": "Registrer", "E-mail": "E-mail", - "Google verification code": "Google-verifikationskode", "Preferences": "Præferencer", "preferences_category_player": "Afspillerindstillinger", "preferences_video_loop_label": "Altid gentag: ", @@ -159,17 +156,12 @@ "Hide replies": "Skjul svar", "Show replies": "Vis svar", "Incorrect password": "Forkert adgangskode", - "Quota exceeded, try again in a few hours": "Kvota overskredet, prøv igen om et par timer", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Login fejlet, tjek at totrinsbekræftelse (Authenticator eller SMS) er slået til.", - "Invalid TFA code": "Ugyldig TFA kode", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "Login fejlede. Dette kan skyldes, at to-faktor autentificering ikke er aktiveret for din konto.", "Wrong answer": "Forkert svar", "Erroneous CAPTCHA": "Fejlagtig CAPTCHA", "CAPTCHA is a required field": "CAPTCHA er et obligatorisk felt", "User ID is a required field": "Bruger ID er et krævet felt", "Password is a required field": "Adgangskode er et obligatorisk felt", "Wrong username or password": "Forkert brugernavn eller adgangskode", - "Please sign in using 'Log in with Google'": "Log ind via 'Log ind med Google'", "Password cannot be empty": "Adgangskoden må ikke være tom", "Password cannot be longer than 55 characters": "Adgangskoden må ikke være længere end 55 tegn", "Please log in": "Venligst log ind", diff --git a/locales/de.json b/locales/de.json index 3c1120c0..5703a0d7 100644 --- a/locales/de.json +++ b/locales/de.json @@ -14,7 +14,6 @@ "Clear watch history?": "Verlauf löschen?", "New password": "Neues Passwort", "New passwords must match": "Neue Passwörter müssen übereinstimmen", - "Cannot change password for Google accounts": "Ich kann das Passwort deines Google Kontos nicht ändern", "Authorize token?": "Token autorisieren?", "Authorize token for `x`?": "Token für `x` autorisieren?", "Yes": "Ja", @@ -37,7 +36,6 @@ "source": "Quelle", "Log in": "Anmelden", "Log in/register": "Anmelden/registrieren", - "Log in with Google": "Mit Google anmelden", "User ID": "Benutzer-ID", "Password": "Passwort", "Time (h:mm:ss):": "Zeit (h:mm:ss):", @@ -46,7 +44,6 @@ "Sign In": "Anmelden", "Register": "Registrieren", "E-mail": "E-Mail", - "Google verification code": "Google-Bestätigungscode", "Preferences": "Einstellungen", "preferences_category_player": "Wiedergabeeinstellungen", "preferences_video_loop_label": "Immer wiederholen: ", @@ -164,17 +161,12 @@ "Hide replies": "Antworten verstecken", "Show replies": "Antworten anzeigen", "Incorrect password": "Falsches Passwort", - "Quota exceeded, try again in a few hours": "Kontingent überschritten, versuche es in ein paar Stunden erneut", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Anmeldung nicht möglich, stellen Sie sicher, dass die Zwei-Faktor-Authentisierung (Authenticator oder SMS) aktiviert ist.", - "Invalid TFA code": "Ungültiger TFA Code", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "Die Anmeldung ist fehlgeschlagen. Dies kann daran liegen, dass die Zwei-Faktor-Authentisierung für Ihr Konto nicht aktiviert ist.", "Wrong answer": "Ungültige Antwort", "Erroneous CAPTCHA": "Ungültiges CAPTCHA", "CAPTCHA is a required field": "CAPTCHA ist eine erforderliche Eingabe", "User ID is a required field": "Benutzer ID ist eine erforderliche Eingabe", "Password is a required field": "Passwort ist eine erforderliche Eingabe", "Wrong username or password": "Ungültiger Benutzername oder Passwort", - "Please sign in using 'Log in with Google'": "Bitte melden Sie sich mit „Mit Google anmelden“ an", "Password cannot be empty": "Passwort darf nicht leer sein", "Password cannot be longer than 55 characters": "Passwort darf nicht länger als 55 Zeichen sein", "Please log in": "Bitte anmelden", diff --git a/locales/el.json b/locales/el.json index 8d0c84dd..13cff649 100644 --- a/locales/el.json +++ b/locales/el.json @@ -14,7 +14,6 @@ "Clear watch history?": "Διαγραφή ιστορικού προβολής;", "New password": "Νέος κωδικός πρόσβασης", "New passwords must match": "Οι νέοι κωδικοί πρόσβασης πρέπει να ταιριάζουν", - "Cannot change password for Google accounts": "Δεν επιτρέπεται η αλλαγή κωδικού πρόσβασης λογαριασμών Google", "Authorize token?": "Εξουσιοδότηση διασύνδεσης;", "Authorize token for `x`?": "Εξουσιοδότηση διασύνδεσης με `x`;", "Yes": "Ναι", @@ -37,7 +36,6 @@ "source": "πηγή", "Log in": "Σύνδεση", "Log in/register": "Σύνδεση/εγγραφή", - "Log in with Google": "Σύνδεση με Google", "User ID": "Ταυτότητα χρήστη", "Password": "Κωδικός πρόσβασης", "Time (h:mm:ss):": "Ώρα (ω:λλ:δδ):", @@ -46,7 +44,6 @@ "Sign In": "Σύνδεση", "Register": "Εγγραφή", "E-mail": "Ηλεκτρονικό ταχυδρομείο", - "Google verification code": "Κωδικός επαλήθευσης Google", "Preferences": "Προτιμήσεις", "preferences_category_player": "Προτιμήσεις αναπαραγωγής", "preferences_video_loop_label": "Αυτόματη επανάληψη: ", @@ -155,17 +152,12 @@ "Hide replies": "Απόκρυψη απαντήσεων", "Show replies": "Προβολή απαντήσεων", "Incorrect password": "Λανθασμένος κωδικός πρόσβασης", - "Quota exceeded, try again in a few hours": "Έχετε υπερβεί το όριο προσπαθειών, δοκιμάστε ξανα σε λίγες ώρες", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Αδυναμία σύνδεσης, βεβαιωθείτε πως ο έλεγχος ταυτότητας δύο παραγόντων (με Authenticator ή SMS) είναι ενεργοποιημένος.", - "Invalid TFA code": "Μη έγκυρος κωδικός ελέγχου ταυτότητας δύο παραγόντων", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "Αποτυχία σύνδεσης. Ίσως ευθύνεται η έλλειψη ελέγχου ταυτότητας δύο παραγόντων για το λογαριασμό σας.", "Wrong answer": "Λανθασμένη απάντηση", "Erroneous CAPTCHA": "Λανθασμένο CAPTCHA", "CAPTCHA is a required field": "Το CAPTCHA είναι απαιτούμενο πεδίο", "User ID is a required field": "Η ταυτότητα χρήστη είναι απαιτούμενο πεδίο", "Password is a required field": "Ο κωδικός πρόσβασης είναι απαιτούμενο πεδίο", "Wrong username or password": "Λανθασμένο όνομα χρήστη ή κωδικός πρόσβασης", - "Please sign in using 'Log in with Google'": "Συνδεθείτε με την επιλογή 'Σύνδεση με Google'", "Password cannot be empty": "Ο κωδικός πρόσβασης δεν γίνεται να είναι κενός", "Password cannot be longer than 55 characters": "Ο κωδικός πρόσβασης δεν γίνεται να υπερβαίνει τους 55 χαρακτήρες", "Please log in": "Συνδεθείτε", diff --git a/locales/en-US.json b/locales/en-US.json index 96b6799b..e13ba968 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -24,7 +24,6 @@ "Clear watch history?": "Clear watch history?", "New password": "New password", "New passwords must match": "New passwords must match", - "Cannot change password for Google accounts": "Cannot change password for Google accounts", "Authorize token?": "Authorize token?", "Authorize token for `x`?": "Authorize token for `x`?", "Yes": "Yes", @@ -48,7 +47,6 @@ "source": "source", "Log in": "Log in", "Log in/register": "Log in/register", - "Log in with Google": "Log in with Google", "User ID": "User ID", "Password": "Password", "Time (h:mm:ss):": "Time (h:mm:ss):", @@ -57,7 +55,6 @@ "Sign In": "Sign In", "Register": "Register", "E-mail": "E-mail", - "Google verification code": "Google verification code", "Preferences": "Preferences", "preferences_category_player": "Player preferences", "preferences_video_loop_label": "Always loop: ", @@ -208,17 +205,12 @@ "Hide replies": "Hide replies", "Show replies": "Show replies", "Incorrect password": "Incorrect password", - "Quota exceeded, try again in a few hours": "Quota exceeded, try again in a few hours", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.", - "Invalid TFA code": "Invalid TFA code", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "Login failed. This may be because two-factor authentication is not turned on for your account.", "Wrong answer": "Wrong answer", "Erroneous CAPTCHA": "Erroneous CAPTCHA", "CAPTCHA is a required field": "CAPTCHA is a required field", "User ID is a required field": "User ID is a required field", "Password is a required field": "Password is a required field", "Wrong username or password": "Wrong username or password", - "Please sign in using 'Log in with Google'": "Please sign in using 'Log in with Google'", "Password cannot be empty": "Password cannot be empty", "Password cannot be longer than 55 characters": "Password cannot be longer than 55 characters", "Please log in": "Please log in", diff --git a/locales/eo.json b/locales/eo.json index 4e789390..a4b46bef 100644 --- a/locales/eo.json +++ b/locales/eo.json @@ -14,7 +14,6 @@ "Clear watch history?": "Ĉu forigi vidohistorion?", "New password": "Nova pasvorto", "New passwords must match": "Novaj pasvortoj devas kongrui", - "Cannot change password for Google accounts": "Ne eblas ŝanĝi pasvorton por kontoj de Google", "Authorize token?": "Ĉu rajtigi ĵetonon?", "Authorize token for `x`?": "Ĉu rajtigi ĵetonon por `x`?", "Yes": "Jes", @@ -37,7 +36,6 @@ "source": "fonto", "Log in": "Ensaluti", "Log in/register": "Ensaluti/Registriĝi", - "Log in with Google": "Ensaluti al Google", "User ID": "Uzula identigilo", "Password": "Pasvorto", "Time (h:mm:ss):": "Horo (h:mm:ss):", @@ -46,7 +44,6 @@ "Sign In": "Ensaluti", "Register": "Registriĝi", "E-mail": "Retpoŝto", - "Google verification code": "Kontrolkodo de Google", "Preferences": "Agordoj", "preferences_category_player": "Spektilaj agordoj", "preferences_video_loop_label": "Ĉiam ripeti: ", @@ -164,17 +161,12 @@ "Hide replies": "Kaŝi respondojn", "Show replies": "Montri respondojn", "Incorrect password": "Malbona pasvorto", - "Quota exceeded, try again in a few hours": "Kvoto transpasita, provu denove post iuj horoj", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Ne povas ensaluti, certigu, ke dufaktora aŭtentigo (Authenticator aŭ SMS) estas ebligita.", - "Invalid TFA code": "Nevalida TFA-kodo", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "Ensalutado fiaskis. Eble ĉar la dufaktora aŭtentigo estas malebligita en via konto.", "Wrong answer": "Nevalida respondo", "Erroneous CAPTCHA": "Nevalida CAPTCHA", "CAPTCHA is a required field": "CAPTCHA estas deviga kampo", "User ID is a required field": "Uzula identigilo estas deviga kampo", "Password is a required field": "Pasvorto estas deviga kampo", "Wrong username or password": "Nevalida uzantnomo aŭ pasvorto", - "Please sign in using 'Log in with Google'": "Bonvolu ensaluti per 'Ensaluti per Google'", "Password cannot be empty": "Pasvorto ne povas esti malplena", "Password cannot be longer than 55 characters": "Pasvorto ne povas esti pli longa ol 55 signoj", "Please log in": "Bonvolu ensaluti", diff --git a/locales/es.json b/locales/es.json index 76145a67..b3103a25 100644 --- a/locales/es.json +++ b/locales/es.json @@ -14,7 +14,6 @@ "Clear watch history?": "¿Quiere borrar el historial de reproducción?", "New password": "Nueva contraseña", "New passwords must match": "Las nuevas contraseñas deben coincidir", - "Cannot change password for Google accounts": "No se puede cambiar la contraseña de la cuenta de Google", "Authorize token?": "¿Autorizar el token?", "Authorize token for `x`?": "¿Autorizar el token para `x`?", "Yes": "Sí", @@ -37,7 +36,6 @@ "source": "código fuente", "Log in": "Iniciar sesión", "Log in/register": "Iniciar sesión/Registrarse", - "Log in with Google": "Iniciar sesión en Google", "User ID": "Nombre", "Password": "Contraseña", "Time (h:mm:ss):": "Hora (h:mm:ss):", @@ -46,7 +44,6 @@ "Sign In": "Iniciar sesión", "Register": "Registrarse", "E-mail": "Correo", - "Google verification code": "Código de verificación de Google", "Preferences": "Preferencias", "preferences_category_player": "Preferencias del reproductor", "preferences_video_loop_label": "Repetir siempre: ", @@ -164,17 +161,12 @@ "Hide replies": "Ocultar las respuestas", "Show replies": "Mostrar las respuestas", "Incorrect password": "Contraseña incorrecta", - "Quota exceeded, try again in a few hours": "Cuota excedida, prueba otra vez en unas horas", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "No se puede iniciar sesión, asegúrese de que la autentificación de dos factores (autentificador o SMS) esté habilitada.", - "Invalid TFA code": "Código TFA no válido", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "Error de inicio de sesion. Puede deberse a que la autentificación de dos factores no está habilitada en su cuenta.", "Wrong answer": "Respuesta no válida", "Erroneous CAPTCHA": "CAPTCHA no válido", "CAPTCHA is a required field": "El CAPTCHA es un campo obligatorio", "User ID is a required field": "El nombre es un campo obligatorio", "Password is a required field": "La contraseña es un campo obligatorio", "Wrong username or password": "Nombre o contraseña incorrecto", - "Please sign in using 'Log in with Google'": "Inicie sesión con «Iniciar sesión con Google»", "Password cannot be empty": "La contraseña no puede estar en blanco", "Password cannot be longer than 55 characters": "La contraseña no debe tener más de 55 caracteres", "Please log in": "Inicie sesión, por favor", diff --git a/locales/et.json b/locales/et.json index 74338aba..7f652810 100644 --- a/locales/et.json +++ b/locales/et.json @@ -25,7 +25,6 @@ "Clear watch history?": "Kustuta vaatamiste ajalugu?", "New password": "Uus salasõna", "New passwords must match": "Uued salasõnad peavad ühtima", - "Cannot change password for Google accounts": "Google'i kasutaja salasõna ei saa muuta", "Import and Export Data": "Impordi ja ekspordi andmed", "Import": "Impordi", "Import YouTube subscriptions": "Impordi tellimused Youtube'ist/OPML-ist", @@ -38,7 +37,6 @@ "History": "Ajalugu", "JavaScript license information": "JavaScripti litsentsi info", "source": "allikas", - "Log in with Google": "Logi sisse Google'iga", "User ID": "Kasutada ID", "Password": "Salasõna", "Time (h:mm:ss):": "Aeg (h:mm:ss):", @@ -118,12 +116,10 @@ "Hide replies": "Peida vastused", "Show replies": "Näita vastuseid", "Incorrect password": "Vale salasõna", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "Sisselogimine ei õnnestunud. Asi võib olla selles, et", "Wrong answer": "Vale vastus", "User ID is a required field": "Kasutaja ID on kohustuslik väli", "Password is a required field": "Salasõna on kohustuslik väli", "Wrong username or password": "Vale kasutajanimi või salasõna", - "Please sign in using 'Log in with Google'": "Palun kasutage 'Logi sisse Google'iga'", "Password cannot be longer than 55 characters": "Salasõna ei tohi olla pikem kui 55 tähemärki", "Password cannot be empty": "Salasõna ei tohi olla tühi", "Please log in": "Palun logige sisse", @@ -290,8 +286,6 @@ "": "Vaata `x` kommentaare" }, "Khmer": "Khmeeri", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Sisselogimine ei õnnestunud. Kontrollige, kas two-factor authentication (Authenticator või SMS) on sisselülitatud.", - "Invalid TFA code": "Vale TFA-kood", "Bosnian": "Bosnia", "Corsican": "Korsika", "Javanese": "Jaava", diff --git a/locales/eu.json b/locales/eu.json index 9e093a52..8b365270 100644 --- a/locales/eu.json +++ b/locales/eu.json @@ -14,7 +14,6 @@ "Clear watch history?": "Garbitu ikusitakoen historia?", "New password": "Pasahitz berria", "New passwords must match": "Pasahitza berriek bat egin behar dute", - "Cannot change password for Google accounts": "Ezin da pasahitza aldatu Google kontuetan", "Authorize token?": "Baimendu tokena?", "Yes": "Bai", "No": "Ez", @@ -36,7 +35,6 @@ "source": "iturburua", "Log in": "Saioa hasi", "Log in/register": "Hasi saioa / Eman izena", - "Log in with Google": "Hasi saioa Googlekin", "User ID": "Erabiltzaile IDa", "Password": "Pasahitza", "Time (h:mm:ss):": "Denbora (h:mm:ss):", @@ -93,7 +91,6 @@ "Import/export data": "Inportatu/exportatu data", "Create playlist": "Zerrenda sortu", "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Aditu! JavaScript itzalita dakazula ematen du. Hemen sakatu iruzkinak ikusteko. Denbora luza leikeela kontuan hartu.", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Ezinezkoa izena eman. Ziurtatu berresteko bi faktoreak (Authenticator edo SMS) piztuta daudela.", "generic_views_count": "{{count}}ikusia", "generic_views_count_plural": "{{count}}ikusiak", "generic_playlists_count": "{{count}}zerrenda", @@ -136,7 +133,6 @@ "License: ": "Lizentzia: ", "Family friendly? ": "Adeikorra familiarekin? ", "Wilson score: ": "Wilsonen puntuazioa: ", - "Quota exceeded, try again in a few hours": "Kuota gaindituta, ordu batzuren bueltan berriro saiatu", "comments_view_x_replies": "{{count}} erantzuna ikusi", "comments_view_x_replies_plural": "{{count}} erantzunak ikusi", "Catalan": "Katalaniera", @@ -204,7 +200,6 @@ "preferences_category_data": "Dataren lehentasunak", "preferences_default_home_label": "Homepage lehenetsia: ", "preferences_automatic_instance_redirect_label": "berbideratze adibide automatikoa (atzera egin berbideratzeko: invidious.io) ", - "Please sign in using 'Log in with Google'": "'Log in Googlerekin' erabili", "`x` uploaded a video": "' x'(e)k bideo bat igo du", "published - reverse": "argitaratuta - alderantziz", "Could not get channel info.": "Kanalaren adierazpena ezin lortu.", @@ -220,7 +215,6 @@ "Premieres in `x`": "'x'eko estrenaldiak", "Delete playlist `x`?": "'x' zerrenda ezabatu nahi?", "Token is expired, please try again": "Token kadukatua, saiatu berriro", - "Invalid TFA code": "TFA kodea ez da zuzena", "CAPTCHA enabled: ": "CAPTCHA gaitu: ", "Released under the AGPLv3 on Github.": "GitHubeko AGPLv3pean argitaratuta.", "channel:`x`": "Kanal: 'x'", @@ -242,9 +236,7 @@ "preferences_category_subscription": "Harpidetzaren lehentasunak", "Hidden field \"challenge\" is a required field": "\"challenge\" eremu ezkutua beharrezkoa da", "German": "Alemaniarra", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "Ezin izena eman. Izan leike zure konturako berresteko bi faktoreak piztuta ez daudela.", "View YouTube comments": "YouTubeko iruzkinak ikusi", - "Google verification code": "Googleren berresteko kodea", "`x` is live": "'x' bizirik darrai", "Password cannot be empty": "Pasahitza ezin da hutsik utzi", "preferences_video_loop_label": "Beti begiztatu: ", diff --git a/locales/fa.json b/locales/fa.json index 29a0c527..9b6c625d 100644 --- a/locales/fa.json +++ b/locales/fa.json @@ -19,7 +19,6 @@ "Clear watch history?": "پاک کردن تاریخچه نمایش؟", "New password": "گذرواژه تازه", "New passwords must match": "گذارواژه های تازه باید باهم همخوانی داشته باشند", - "Cannot change password for Google accounts": "نمیتوان گذرواژه را برای حساب های کاربری گوگل تغییر داد", "Authorize token?": "توکن دسترسی؟", "Authorize token for `x`?": "توکن دسترسی برای `x`؟", "Yes": "بله", @@ -42,7 +41,6 @@ "source": "منبع", "Log in": "ورود", "Log in/register": "ورود/ثبت نام", - "Log in with Google": "ورود با گوگل", "User ID": "شناسه کاربری", "Password": "گذرواژه", "Time (h:mm:ss):": "زمان (h:mm:ss):", @@ -51,7 +49,6 @@ "Sign In": "ورود", "Register": "ثبت نام", "E-mail": "ایمیل", - "Google verification code": "کد تایید گوگل", "Preferences": "ترجیحات", "preferences_category_player": "ترجیحات نمایش‌دهنده", "preferences_video_loop_label": "همواره ویدئو را بازپخش کن ", @@ -171,17 +168,12 @@ "Hide replies": "مخفی کردن پاسخ ها", "Show replies": "نمایش پاسخ ها", "Incorrect password": "گذرواژه نا درست", - "Quota exceeded, try again in a few hours": "سهمیه بیشتر شده است، چند ساعت بعد دوباره تلاش کنید", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "قادر به ورود نیستید، مطمئن شوید احراز تایید-دو‌مرحله (Authenticator یا پیام‌کوتاه) خاموش باشد.", - "Invalid TFA code": "کد TFA نادرست است", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "ورود با خطا مواجه شد. این ممکن است به خاطر احراز تایید-دو‌مرحله باشد که برای حساب کاربری شما فعال نشده است.", "Wrong answer": "پاسخ غلط", "Erroneous CAPTCHA": "CAPTCHA نا درست", "CAPTCHA is a required field": "CAPTCHA یک فیلد ضروری است", "User ID is a required field": "شناسه کاربری یک فیلد ضروری است", "Password is a required field": "گذرواژه یک فیلد ضروری است", "Wrong username or password": "نام کاربری یا گذرواژه غلط است", - "Please sign in using 'Log in with Google'": "لطفا با استفاده از 'ورود توسط گوگل' وارد شوید", "Password cannot be empty": "گذرواژه نمیتواند خالی باشد", "Password cannot be longer than 55 characters": "گذر واژه نمیتواند از ۵۵ کاراکتر بیشتر باشد", "Please log in": "لطفا وارد شوید", diff --git a/locales/fi.json b/locales/fi.json index 366a2739..5d8578a5 100644 --- a/locales/fi.json +++ b/locales/fi.json @@ -14,7 +14,6 @@ "Clear watch history?": "Tyhjennä katseluhistoria?", "New password": "Uusi salasana", "New passwords must match": "Uusien salasanojen täytyy täsmätä", - "Cannot change password for Google accounts": "Google-tilien salasanaa ei voi vaihtaa", "Authorize token?": "Valuutetaanko tunnus?", "Authorize token for `x`?": "Valtuutetaanko tunnus `x`:lle?", "Yes": "Kyllä", @@ -37,7 +36,6 @@ "source": "lähde", "Log in": "Kirjaudu sisään", "Log in/register": "Kirjaudu sisään/rekisteröidy", - "Log in with Google": "Kirjaudu sisään Googlella", "User ID": "Käyttäjätunnus", "Password": "Salasana", "Time (h:mm:ss):": "Aika (h:mm:ss):", @@ -46,7 +44,6 @@ "Sign In": "Kirjaudu sisään", "Register": "Rekisteröidy", "E-mail": "Sähköposti", - "Google verification code": "Google-vahvistuskoodi", "Preferences": "Asetukset", "preferences_category_player": "Soittimen asetukset", "preferences_video_loop_label": "Toista jatkuvasti aina: ", @@ -163,17 +160,12 @@ "Hide replies": "Piilota vastaukset", "Show replies": "Näytä vastaukset", "Incorrect password": "Väärä salasana", - "Quota exceeded, try again in a few hours": "Kiintiö ylitetty, yritä parin tunnin kuluttua uudestaan", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Sisäänkirjautuminen epäonnistui. Varmista, että kaksivaiheinen tunnistautuminen (Authenticator tai tekstiviesti) on käytössä.", - "Invalid TFA code": "Virheellinen turvakoodi", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "Sisäänkirjautuminen epäonnistui. Tämä voi johtua siitä, että kaksivaiheinen tunnistautuminen on pois käytöstä tunnuksellasi.", "Wrong answer": "Väärä vastaus", "Erroneous CAPTCHA": "Virheellinen CAPTCHA", "CAPTCHA is a required field": "CAPTCHA-kenttä vaaditaan", "User ID is a required field": "Käyttäjätunnus vaaditaan", "Password is a required field": "Salasana vaaditaan", "Wrong username or password": "Väärä käyttäjänimi tai salasana", - "Please sign in using 'Log in with Google'": "Ole hyvä ja kirjaudu sisään Google-tunnuksella", "Password cannot be empty": "Salasana ei voi olla tyhjä", "Password cannot be longer than 55 characters": "Salasana ei voi olla yli 55 merkkiä pitkä", "Please log in": "Kirjaudu sisään, ole hyvä", diff --git a/locales/fr.json b/locales/fr.json index 29895703..d2607a49 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -24,7 +24,6 @@ "Clear watch history?": "Êtes-vous sûr de vouloir supprimer l'historique des vidéos regardées ?", "New password": "Nouveau mot de passe", "New passwords must match": "Les nouveaux mots de passe doivent correspondre", - "Cannot change password for Google accounts": "Le mot de passe d'un compte Google ne peut pas être changé depuis Invidious", "Authorize token?": "Autoriser le token ?", "Authorize token for `x`?": "Autoriser le token pour `x` ?", "Yes": "Oui", @@ -47,7 +46,6 @@ "source": "source", "Log in": "Se connecter", "Log in/register": "Se connecter/S'inscrire", - "Log in with Google": "Se connecter avec Google", "User ID": "Identifiant utilisateur", "Password": "Mot de passe", "Time (h:mm:ss):": "Heure (h:mm:ss) :", @@ -56,7 +54,6 @@ "Sign In": "Se connecter", "Register": "S'inscrire", "E-mail": "E-mail", - "Google verification code": "Code de vérification Google", "Preferences": "Préférences", "preferences_category_player": "Préférences du lecteur", "preferences_video_loop_label": "Lire en boucle : ", @@ -179,17 +176,12 @@ "Hide replies": "Masquer les réponses", "Show replies": "Afficher les réponses", "Incorrect password": "Mot de passe incorrect", - "Quota exceeded, try again in a few hours": "Nombre de tentatives de connexion dépassé, réessayez dans quelques heures", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Impossible de se connecter, si après plusieurs tentative vous ne parvenez toujours pas à vous connecter, assurez-vous que l'authentification à deux facteurs (Authenticator ou SMS) est activée.", - "Invalid TFA code": "Code d'authentification à deux facteurs invalide", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "La connexion a échoué. Cela peut être dû au fait que l'authentification à deux facteurs n'est pas activée sur votre compte.", "Wrong answer": "Réponse invalide", "Erroneous CAPTCHA": "CAPTCHA invalide", "CAPTCHA is a required field": "Veuillez entrer un CAPTCHA", "User ID is a required field": "Veuillez entrer un Identifiant Utilisateur", "Password is a required field": "Veuillez entrer un Mot de passe", "Wrong username or password": "Nom d'utilisateur ou mot de passe invalide", - "Please sign in using 'Log in with Google'": "Veuillez vous connecter en utilisant \"Se connecter avec Google\"", "Password cannot be empty": "Le mot de passe ne peut pas être vide", "Password cannot be longer than 55 characters": "Le mot de passe ne doit pas comporter plus de 55 caractères", "Please log in": "Veuillez vous connecter", diff --git a/locales/he.json b/locales/he.json index ab42313b..6fee93b2 100644 --- a/locales/he.json +++ b/locales/he.json @@ -14,7 +14,6 @@ "Clear watch history?": "לנקות את היסטוריית הצפייה?", "New password": "סיסמה חדשה", "New passwords must match": "על הסיסמאות החדשות להתאים", - "Cannot change password for Google accounts": "לא ניתן לשנות את הסיסמה לחשבונות Google", "Authorize token?": "לאשר את האסימון?", "Authorize token for `x`?": "האם לאשר את האסימון עבור `x`?", "Yes": "כן", @@ -37,7 +36,6 @@ "source": "source", "Log in": "כניסה", "Log in/register": "כניסה/הרשמה", - "Log in with Google": "כניסה עם Google", "User ID": "שם משתמש", "Password": "סיסמה", "Time (h:mm:ss):": "זמן (h:mm:ss):", @@ -46,7 +44,6 @@ "Sign In": "התחברות", "Register": "הרשמה", "E-mail": "דוא״ל", - "Google verification code": "קוד האימות של Google", "Preferences": "העדפות", "preferences_category_player": "העדפות הנגן", "preferences_autoplay_label": "ניגון אוטומטי: ", @@ -137,7 +134,6 @@ "User ID is a required field": "חובה למלא את שדה שם המשתמש", "Password is a required field": "חובה למלא את שדה הסיסמה", "Wrong username or password": "שם משתמש שגוי או סיסמה שגויה", - "Please sign in using 'Log in with Google'": "נא להתחבר בעזרת \"התחברות עם Google\"", "Password cannot be longer than 55 characters": "על אורך הסיסמה להיות 55 תווים לכל היותר", "Please log in": "נא להתחבר", "channel:`x`": "ערוץ:`x`", diff --git a/locales/hi.json b/locales/hi.json index 41335266..dcb7294d 100644 --- a/locales/hi.json +++ b/locales/hi.json @@ -4,7 +4,6 @@ "No": "नहीं", "Export subscriptions as OPML (for NewPipe & FreeTube)": "OPML के रूप में सदस्यताएँ निर्यात करें (NewPipe और FreeTube के लिए)", "Log in/register": "लॉग-इन/पंजीकृत करें", - "Log in with Google": "Google के साथ लॉग-इन करें", "preferences_autoplay_label": "अपने आप चलाने की सुविधा: ", "preferences_dark_mode_label": "थीम: ", "preferences_default_home_label": "डिफ़ॉल्ट मुखपृष्ठ: ", @@ -58,7 +57,6 @@ "Clear watch history?": "देखने का इतिहास मिटाएँ?", "New password": "नया पासवर्ड", "New passwords must match": "पासवर्ड्स को मेल खाना होगा", - "Cannot change password for Google accounts": "Google खातों के लिए पासवर्ड नहीं बदल सकते", "Authorize token?": "टोकन को प्रमाणित करें?", "Authorize token for `x`?": "`x` के लिए टोकन को प्रमाणित करें?", "Import and Export Data": "डेटा को आयात और निर्यात करें", @@ -81,7 +79,6 @@ "Password": "पासवर्ड", "Register": "पंजीकृत करें", "E-mail": "ईमेल", - "Google verification code": "Google प्रमाणीकरण कोड", "Time (h:mm:ss):": "समय (घं:मिमि:सेसे):", "Text CAPTCHA": "टेक्स्ट CAPTCHA", "Image CAPTCHA": "चित्र CAPTCHA", @@ -224,15 +221,10 @@ "Hide replies": "जवाब छिपाएँ", "Show replies": "जवाब दिखाएँ", "Incorrect password": "गलत पासवर्ड", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "लॉग-इन नहीं किया जा सका, सुनिश्चित करें कि दो-कारक प्रमाणीकरण (Authenticator या SMS) सक्षम है।", - "Invalid TFA code": "अमान्य TFA कोड", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "लॉग-इन नाकाम रहा। ऐसा इसलिए हो सकता है कि दो-कारक प्रमाणीकरण आपके खाते पर सक्षम नहीं है।", - "Quota exceeded, try again in a few hours": "कोटा पार हो चुका है, कृपया कुछ घंटों में फिर कोशिश करें", "CAPTCHA is a required field": "CAPTCHA एक ज़रूरी फ़ील्ड है", "User ID is a required field": "सदस्य ID एक ज़रूरी फ़ील्ड है", "Password is a required field": "पासवर्ड एक ज़रूरी फ़ील्ड है", "Wrong username or password": "गलत सदस्यनाम या पासवर्ड", - "Please sign in using 'Log in with Google'": "कृपया 'Google के साथ लॉग-इन करें' के साथ साइन-इन करें", "Password cannot be empty": "पासवर्ड खाली नहीं हो सकता", "Password cannot be longer than 55 characters": "पासवर्ड में अधिकतम 55 अक्षर हो सकते हैं", "Invidious Private Feed for `x`": "`x` के लिए Invidious निजी फ़ीड", diff --git a/locales/hr.json b/locales/hr.json index 46e07b83..0549fa70 100644 --- a/locales/hr.json +++ b/locales/hr.json @@ -14,7 +14,6 @@ "Clear watch history?": "Izbrisati povijest gledanja?", "New password": "Nova lozinka", "New passwords must match": "Nove lozinke se moraju poklapati", - "Cannot change password for Google accounts": "Nije moguće promijeniti lozinku za Google račune", "Authorize token?": "Autorizirati token?", "Authorize token for `x`?": "Autorizirati token za `x`?", "Yes": "Da", @@ -37,7 +36,6 @@ "source": "izvor", "Log in": "Prijavi se", "Log in/register": "Prijavi se/registriraj se", - "Log in with Google": "Prijavi se pomoću Googlea", "User ID": "Korisnički ID", "Password": "Lozinka", "Time (h:mm:ss):": "Vrijeme (h:mm:ss):", @@ -46,7 +44,6 @@ "Sign In": "Prijavi se", "Register": "Registriraj se", "E-mail": "E-mail adresa", - "Google verification code": "Googleov potvrdni kod", "Preferences": "Postavke", "preferences_category_player": "Postavke playera", "preferences_video_loop_label": "Uvijek ponavljaj: ", @@ -164,17 +161,12 @@ "Hide replies": "Sakrij odgovore", "Show replies": "Prikaži odgovore", "Incorrect password": "Neispravna lozinka", - "Quota exceeded, try again in a few hours": "Kvota je prekoračena. Pokušaj ponovo za par sati", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Prijava neuspjela. Provjeri da je dvofaktorska autentifikacija uključena (Authenticator ili SMS).", - "Invalid TFA code": "Neispravan TFA kod", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "Prijava neuspjela. Možda zato što za tvoj račun nije uključena dvofaktorska autentifikacija.", "Wrong answer": "Krivi odgovor", "Erroneous CAPTCHA": "Neispravan CAPTCHA", "CAPTCHA is a required field": "CAPTCHA je obavezno polje", "User ID is a required field": "Korisnički ID je obavezno polje", "Password is a required field": "Polje lozinke je obavezno polje", "Wrong username or password": "Krivo korisničko ime ili lozinka", - "Please sign in using 'Log in with Google'": "Za prijavu koristi „Prijavi se pomoću Googlea”", "Password cannot be empty": "Polje lozinke ne smije ostati prazno", "Password cannot be longer than 55 characters": "Lozinka ne može biti duža od 55 znakova", "Please log in": "Prijavi se", diff --git a/locales/hu-HU.json b/locales/hu-HU.json index f93930e0..1899b71c 100644 --- a/locales/hu-HU.json +++ b/locales/hu-HU.json @@ -24,7 +24,6 @@ "Clear watch history?": "Törölve legyen a megnézett videók naplója?", "New password": "Új jelszó", "New passwords must match": "Az új jelszavaknak egyezniük kell.", - "Cannot change password for Google accounts": "A Google-fiók jelszavát nem lehet megváltoztatni.", "Authorize token?": "Engedélyezve legyen a token?", "Authorize token for `x`?": "Engedélyezve legyen a token erre? „`x`”", "Yes": "Igen", @@ -47,7 +46,6 @@ "source": "forrás", "Log in": "Bejelentkezés", "Log in/register": "Bejelentkezés/Regisztrálás", - "Log in with Google": "Bejelentkezés Google-fiókkal", "User ID": "Felhasználói azonosító", "Password": "Jelszó", "Time (h:mm:ss):": "A pontos idő (ó:pp:mm):", @@ -56,7 +54,6 @@ "Sign In": "Bejelentkezés", "Register": "Regisztrálás", "E-mail": "E-mail-cím", - "Google verification code": "A Google ellenőrző kódja", "Preferences": "Beállítások", "preferences_category_player": "Lejátszó beállításai", "preferences_video_loop_label": "Videó állandó ismétlése: ", @@ -173,16 +170,12 @@ "Hide replies": "Válaszok elrejtése", "Show replies": "Válaszok mutatása", "Incorrect password": "A jelszó nem megfelelő", - "Quota exceeded, try again in a few hours": "A kvótát meghaladták. Néhány órával később próbáld meg újból betölteni.", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Nem sikerült bejelentkezni. A kétlépcsős (hitelesítő vagy szöveges üzenet általi) hitelesítésnek bekapcsolva kell lennie.", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "Nem sikerült bejelentkezni. Ennek oka lehet, hogy a kétlépcsős hitelesítés nincs bekapcsolva a fiók beállításaiban.", "Wrong answer": "Nem jól válaszoltál.", "Erroneous CAPTCHA": "A CAPTCHA hibás.", "CAPTCHA is a required field": "A CAPTCHA-mezőt ki kell tölteni.", "User ID is a required field": "A felhasználói azonosítót meg kell adni.", "Password is a required field": "Meg kell adni egy jelszót.", "Wrong username or password": "Vagy a felhasználói név, vagy pedig a jelszó nem megfelelő.", - "Please sign in using 'Log in with Google'": "A „Bejelentkezés Google-el” gombbal jelentkezz be.", "Password cannot be empty": "A jelszót nem lehet kihagyni.", "Password cannot be longer than 55 characters": "A jelszó nem lehet hosszabb 55 karakternél.", "Please log in": "Kérjük, jelentkezz be.", @@ -419,7 +412,6 @@ "Switch Invidious Instance": "Váltás másik Invidious-oldalra", "Urdu": "urdu", "search_filters_date_option_week": "Ezen a héten", - "Invalid TFA code": "A kétlépéses hitelesítés kódja nem megfelelő", "footer_documentation": "Dokumentáció", "search_filters_features_option_hd": "HD", "next_steps_error_message_go_to_youtube": "Ugrás a YouTube-ra", diff --git a/locales/id.json b/locales/id.json index f0adfdb1..ef677251 100644 --- a/locales/id.json +++ b/locales/id.json @@ -19,7 +19,6 @@ "Clear watch history?": "Bersihkan riwayat tontonan?", "New password": "Kata sandi baru", "New passwords must match": "Kata sandi baru harus cocok", - "Cannot change password for Google accounts": "Tidak dapat mengganti kata sandi untuk akun Google", "Authorize token?": "Otorisasi token?", "Authorize token for `x`?": "Otorisasi token untuk `x`?", "Yes": "Ya", @@ -42,7 +41,6 @@ "source": "sumber", "Log in": "Masuk", "Log in/register": "Masuk/Daftar", - "Log in with Google": "Masuk dengan Google", "User ID": "ID Pengguna", "Password": "Kata Sandi", "Time (h:mm:ss):": "Waktu (j:mm:dd):", @@ -51,7 +49,6 @@ "Sign In": "Masuk", "Register": "Daftar", "E-mail": "Surel", - "Google verification code": "Kode verifikasi Google", "Preferences": "Preferensi", "preferences_category_player": "Preferensi pemutar", "preferences_video_loop_label": "Selalu ulangi: ", @@ -171,17 +168,12 @@ "Hide replies": "Sembunyikan balasan", "Show replies": "Lihat balasan", "Incorrect password": "Kata sandi salah", - "Quota exceeded, try again in a few hours": "Kuota penuh, coba lagi dalam beberapa jam", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Tidak dapat masuk, pastikan autentikasi dua-faktor (autentikator atau SMS) sudah nyala.", - "Invalid TFA code": "Kode TFA tidak valid", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "Gagal masuk. Ini mungkin disebabkan autentikasi dua-faktor tidak dinyalakan untuk akun Anda.", "Wrong answer": "Jawaban salah", "Erroneous CAPTCHA": "CAPTCHA salah", "CAPTCHA is a required field": "CAPTCHA perlu diisi", "User ID is a required field": "ID pengguna perlu diisi", "Password is a required field": "Kata sandi perlu diisi", "Wrong username or password": "Nama pengguna atau kata sandi salah", - "Please sign in using 'Log in with Google'": "Harap masuk menggunakan 'Masuk dengan Google'", "Password cannot be empty": "Kata sandi tidak boleh kosong", "Password cannot be longer than 55 characters": "Kata sandi tidak boleh lebih dari 55 karakter", "Please log in": "Harap masuk", diff --git a/locales/is.json b/locales/is.json index 3282eb50..ea4c4693 100644 --- a/locales/is.json +++ b/locales/is.json @@ -14,7 +14,6 @@ "Clear watch history?": "Hreinsa áhorfssögu?", "New password": "Nýtt lykilorð", "New passwords must match": "Nýtt lykilorð verður að passa", - "Cannot change password for Google accounts": "Ekki er hægt að breyta lykilorði fyrir Google reikninga", "Authorize token?": "Leyfa tákn?", "Authorize token for `x`?": "Leyfa tákn fyrir `x`?", "Yes": "Já", @@ -37,7 +36,6 @@ "source": "uppspretta", "Log in": "Skrá inn", "Log in/register": "Innskráning/nýskráning", - "Log in with Google": "Skrá inn með Google", "User ID": "Notandakenni", "Password": "Lykilorð", "Time (h:mm:ss):": "Tími (h:mm: ss):", @@ -46,7 +44,6 @@ "Sign In": "Skrá inn", "Register": "Nýskrá", "E-mail": "Tölvupóstur", - "Google verification code": "Google staðfestingarkóði", "Preferences": "Kjörstillingar", "preferences_category_player": "Kjörstillingar spilara", "preferences_video_loop_label": "Alltaf lykkja: ", @@ -155,17 +152,12 @@ "Hide replies": "Fela svör", "Show replies": "Sýna svör", "Incorrect password": "Rangt lykilorð", - "Quota exceeded, try again in a few hours": "Kvóti fór yfir, reyndu aftur eftir nokkrar klukkustundir", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Ekki er hægt að skrá þig inn, vertu viss um að tvíþætt staðfesting (Authenticator eða SMS) sé kveikt á.", - "Invalid TFA code": "Ógildur TFA kóði", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "Innskráning mistókst. Þetta gæti verið vegna þess að tvíþátta staðfesting er ekki kveikt á reikningnum þínum.", "Wrong answer": "Rangt svar", "Erroneous CAPTCHA": "Rangt CAPTCHA", "CAPTCHA is a required field": "CAPTCHA er nauðsynlegur reitur", "User ID is a required field": "Notandakenni er nauðsynlegur reitur", "Password is a required field": "Lykilorð er nauðsynlegur reitur", "Wrong username or password": "Rangt notandanafn eða lykilorð", - "Please sign in using 'Log in with Google'": "Vinsamlegast skráðu þig inn með því að nota 'Innskráning með Google'", "Password cannot be empty": "Lykilorð má ekki vera autt", "Password cannot be longer than 55 characters": "Lykilorð má ekki vera lengra en 55 stafir", "Please log in": "Vinsamlegast skráðu þig inn", diff --git a/locales/it.json b/locales/it.json index 9299add7..a3d0f5da 100644 --- a/locales/it.json +++ b/locales/it.json @@ -20,7 +20,6 @@ "Clear watch history?": "Eliminare la cronologia dei video guardati?", "New password": "Nuova password", "New passwords must match": "Le nuove password devono corrispondere", - "Cannot change password for Google accounts": "Non è possibile modificare la password per gli account Google", "Authorize token?": "Autorizzare gettone?", "Authorize token for `x`?": "Autorizzare gettone per `x`?", "Yes": "Sì", @@ -43,7 +42,6 @@ "source": "sorgente", "Log in": "Accedi", "Log in/register": "Accedi/Registrati", - "Log in with Google": "Accedi con Google", "User ID": "ID utente", "Password": "Password", "Time (h:mm:ss):": "Orario (h:mm:ss):", @@ -52,7 +50,6 @@ "Sign In": "Accedi", "Register": "Registrati", "E-mail": "E-mail", - "Google verification code": "Codice di verifica Google", "Preferences": "Preferenze", "preferences_category_player": "Preferenze del riproduttore", "preferences_video_loop_label": "Ripeti sempre: ", @@ -169,17 +166,12 @@ "Hide replies": "Nascondi le risposte", "Show replies": "Mostra le risposte", "Incorrect password": "Password sbagliata", - "Quota exceeded, try again in a few hours": "Limite superato, prova di nuovo fra qualche ora", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Impossibile autenticarsi, controlla che l'autenticazione in due passaggi (Authenticator o SMS) sia attiva.", - "Invalid TFA code": "Codice di autenticazione a due fattori non valido", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "Login fallito. L'errore potrebbe essere causato dal fatto che la verifica in due passaggi non è attiva sul tuo account.", "Wrong answer": "Risposta errata", "Erroneous CAPTCHA": "CAPTCHA errato", "CAPTCHA is a required field": "Il CAPTCHA è un campo obbligatorio", "User ID is a required field": "L'ID utente è obbligatorio", "Password is a required field": "La password è un campo obbligatorio", "Wrong username or password": "Nome utente o password errati", - "Please sign in using 'Log in with Google'": "Per favore accedi con «Entra con Google»", "Password cannot be empty": "La password non può essere vuota", "Password cannot be longer than 55 characters": "La password non può contenere più di 55 caratteri", "Please log in": "Per favore, accedi", diff --git a/locales/ja.json b/locales/ja.json index 157862c6..80e28460 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -19,7 +19,6 @@ "Clear watch history?": "再生履歴を削除しますか?", "New password": "新しいパスワード", "New passwords must match": "新しいパスワードが一致していません", - "Cannot change password for Google accounts": "Google アカウントのパスワードは変更できません", "Authorize token?": "トークンを認証しますか?", "Authorize token for `x`?": "トークン `x` を認証しますか?", "Yes": "はい", @@ -42,7 +41,6 @@ "source": "ソース", "Log in": "ログイン", "Log in/register": "ログイン/登録", - "Log in with Google": "Google でログイン", "User ID": "ユーザー ID", "Password": "パスワード", "Time (h:mm:ss):": "時間 (時:分分:秒秒):", @@ -51,7 +49,6 @@ "Sign In": "サインイン", "Register": "登録", "E-mail": "メールアドレス", - "Google verification code": "Google 認証コード", "Preferences": "設定", "preferences_category_player": "プレイヤーの設定", "preferences_video_loop_label": "常にループ: ", @@ -171,17 +168,12 @@ "Hide replies": "返信を非表示", "Show replies": "返信を表示", "Incorrect password": "パスワードが間違っています", - "Quota exceeded, try again in a few hours": "試行を制限中です。数時間後にやり直してください", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "ログインできませんでした。2段階認証 (認証アプリまたは SMS) が有効になっていることを確認してください。", - "Invalid TFA code": "TFA (2段階認証) コードが無効です", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "ログインに失敗しました。あなたのアカウントで2段階認証が有効になっていない可能性があります。", "Wrong answer": "回答が間違っています", "Erroneous CAPTCHA": "CAPTCHA が間違っています", "CAPTCHA is a required field": "CAPTCHA は必須項目です", "User ID is a required field": "ユーザー ID は必須項目です", "Password is a required field": "パスワードは必須項目です", "Wrong username or password": "ユーザー名またはパスワードが間違っています", - "Please sign in using 'Log in with Google'": "「Google でログイン」を使用してログインしてください", "Password cannot be empty": "パスワードは空にできません", "Password cannot be longer than 55 characters": "パスワードは55文字より長くできません", "Please log in": "ログインしてください", diff --git a/locales/ko.json b/locales/ko.json index 15357ae4..9c8db5a1 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -32,7 +32,6 @@ "preferences_video_loop_label": "항상 반복: ", "preferences_category_player": "플레이어 설정", "Preferences": "설정", - "Google verification code": "구글 인증 코드", "E-mail": "이메일", "Register": "회원가입", "Sign In": "로그인", @@ -42,7 +41,6 @@ "Time (h:mm:ss):": "시각 (h:mm:ss):", "Password": "비밀번호", "User ID": "사용자 ID", - "Log in with Google": "구글로 로그인", "Log in/register": "로그인/회원가입", "Log in": "로그인", "source": "출처", @@ -65,7 +63,6 @@ "Yes": "예", "Authorize token for `x`?": "`x` 에 대한 토큰을 승인하시겠습니까?", "Authorize token?": "토큰을 승인하시겠습니까?", - "Cannot change password for Google accounts": "구글 계정의 비밀번호를 변경할 수 없습니다", "New passwords must match": "새 비밀번호는 일치해야 합니다", "New password": "새 비밀번호", "Clear watch history?": "재생 기록을 삭제 하시겠습니까?", @@ -112,7 +109,6 @@ "This channel does not exist.": "이 채널은 존재하지 않습니다.", "Deleted or invalid channel": "삭제되었거나 더 이상 존재하지 않는 채널", "channel:`x`": "채널:`x`", - "Invalid TFA code": "유효하지 않은 TFA 코드", "Show replies": "댓글 보기", "Hide replies": "댓글 숨기기", "Incorrect password": "잘못된 비밀번호", @@ -249,7 +245,6 @@ "Engagement: ": "약속: ", "Wilson score: ": "Wilson Score: ", "Family friendly? ": "전연령 영상입니까? ", - "Quota exceeded, try again in a few hours": "한도량을 초과했습니다. 몇 시간 후에 다시 시도하세요", "View `x` comments": { "([^.,0-9]|^)1([^.,0-9]|$)": "`x`개의 댓글 보기", "": "`x`개의 댓글 보기" @@ -272,7 +267,6 @@ "Bulgarian": "불가리아어", "Bosnian": "보스니아어", "Belarusian": "벨라루스어", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "로그인할 수 없습니다. 이중 인증(Authenticator 또는 SMS)이 켜져 있는지 확인하세요.", "View more comments on Reddit": "레딧에서 더 많은 댓글 보기", "View YouTube comments": "유튜브 댓글 보기", "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "자바스크립트가 꺼져 있는 것 같습니다! 댓글을 보려면 여기를 클릭하세요. 댓글을 로드하는 데 시간이 조금 더 걸릴 수 있습니다.", @@ -282,13 +276,11 @@ "Please log in": "로그인하세요", "Password cannot be longer than 55 characters": "비밀번호는 55자 이하여야 합니다", "Password cannot be empty": "비밀번호는 비워둘 수 없습니다", - "Please sign in using 'Log in with Google'": "'구글로 로그인'을 사용하여 로그인하세요", "Wrong username or password": "잘못된 사용자 이름 또는 비밀번호", "Password is a required field": "비밀번호는 필수 입력란입니다", "User ID is a required field": "사용자 ID는 필수 입력란입니다", "CAPTCHA is a required field": "캡차는 필수 입력란입니다", "Erroneous CAPTCHA": "잘못된 캡차", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "로그인 실패. 계정에 이중 인증이 설정되어 있지 않기 때문일 수 있습니다.", "Blacklisted regions: ": "차단된 지역: ", "Playlists": "재생목록", "View as playlist": "재생목록으로 보기", diff --git a/locales/lt.json b/locales/lt.json index 91c7febe..740be7b6 100644 --- a/locales/lt.json +++ b/locales/lt.json @@ -14,7 +14,6 @@ "Clear watch history?": "Išvalyti žiūrėjimo istoriją?", "New password": "Naujas slaptažodis", "New passwords must match": "Naujas slaptažodis turi sutapti", - "Cannot change password for Google accounts": "Negalima pakeisti Google paskyros slaptažodžio", "Authorize token?": "Autorizuoti žetoną?", "Authorize token for `x`?": "Autorizuoti žetoną `x`?", "Yes": "Taip", @@ -37,7 +36,6 @@ "source": "šaltinis", "Log in": "Prisijungti", "Log in/register": "Prisijungti/ registruotis", - "Log in with Google": "Prisijungti naudojantis Google", "User ID": "Naudotojo ID", "Password": "Slaptažodis", "Time (h:mm:ss):": "Laikas (h:mm:ss):", @@ -46,7 +44,6 @@ "Sign In": "Prisijungti", "Register": "Registruotis", "E-mail": "El. paštas", - "Google verification code": "Google patvirtinimo kodas", "Preferences": "Pasirinktys", "preferences_category_player": "Grotuvo pasirinktys", "preferences_video_loop_label": "Visada kartoti: ", @@ -164,17 +161,12 @@ "Hide replies": "Slėpti atsakymus", "Show replies": "Rodyti atsakymus", "Incorrect password": "Slaptažodis neteisingas", - "Quota exceeded, try again in a few hours": "Viršyta kvota, bandykite dar kartą po keleto valandų", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Nepavyko prisijungti, įsitikinkite, kad yra įjungta dviejų etapų autentifikacija (Autentifikatorius arba SMS).", - "Invalid TFA code": "Neteisingas TFA kodas", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "Prisijungimas nepavyko. Tai gali būti todėl, kad jūsų paskyroje nėra įjungta dviejų etapų autentifikacija.", "Wrong answer": "Atsakymas neteisingas", "Erroneous CAPTCHA": "Klaidinga CAPTCHA", "CAPTCHA is a required field": "CAPTCHA yra reikalinga šiam laukeliui", "User ID is a required field": "Vartotojo ID yra reikalingas šiam laukeliui", "Password is a required field": "Slaptažodis yra reikalingas šiam laukeliui", "Wrong username or password": "Neteisingas vartotojo vardas arba slaptažodis", - "Please sign in using 'Log in with Google'": "Prašome prisijungti naudojant \"Prisijungti su\" Google \"", "Password cannot be empty": "Slaptažodžio laukelis negali būti tuščias", "Password cannot be longer than 55 characters": "Slaptažodis negali būti ilgesnis nei 55 simboliai", "Please log in": "Prašome prisijungti", diff --git a/locales/nb-NO.json b/locales/nb-NO.json index d29cca43..05cc7328 100644 --- a/locales/nb-NO.json +++ b/locales/nb-NO.json @@ -14,7 +14,6 @@ "Clear watch history?": "Tøm visningshistorikk?", "New password": "Nytt passord", "New passwords must match": "Nye passordfelter må stemme overens", - "Cannot change password for Google accounts": "Kan ikke endre passord for Google-kontoer", "Authorize token?": "Identitetsbekreft symbol?", "Authorize token for `x`?": "Identitetsbekreft symbol for `x`?", "Yes": "Ja", @@ -37,7 +36,6 @@ "source": "kilde", "Log in": "Logg inn", "Log in/register": "Logg inn/registrer", - "Log in with Google": "Logg inn med Google", "User ID": "Bruker-ID", "Password": "Passord", "Time (h:mm:ss):": "Tid (h:mm:ss):", @@ -46,7 +44,6 @@ "Sign In": "Innlogging", "Register": "Registrer", "E-mail": "E-post", - "Google verification code": "Google-bekreftelseskode", "Preferences": "Innstillinger", "preferences_category_player": "Avspillerinnstillinger", "preferences_video_loop_label": "Alltid gjenta: ", @@ -164,17 +161,12 @@ "Hide replies": "Skjul svar", "Show replies": "Vis svar", "Incorrect password": "Feil passord", - "Quota exceeded, try again in a few hours": "Kvote overskredet, prøv igjen om et par timer", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Kunne ikke logge inn, forsikre deg om at tofaktor-identitetsbekreftelse (Authenticator eller SMS) er skrudd på.", - "Invalid TFA code": "Ugyldig tofaktorkode", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "Innlogging mislyktes. Dette kan være fordi tofaktor-identitetsbekreftelse er skrudd av på kontoen din.", "Wrong answer": "Ugyldig svar", "Erroneous CAPTCHA": "Ugyldig CAPTCHA", "CAPTCHA is a required field": "CAPTCHA er et påkrevd felt", "User ID is a required field": "Bruker-ID er et påkrevd felt", "Password is a required field": "Passord er et påkrevd felt", "Wrong username or password": "Ugyldig brukernavn eller passord", - "Please sign in using 'Log in with Google'": "Logg inn ved bruk av \"Google-innlogging\"", "Password cannot be empty": "Passordet kan ikke være tomt", "Password cannot be longer than 55 characters": "Passordet kan ikke være lengre enn 55 tegn", "Please log in": "Logg inn", diff --git a/locales/nl.json b/locales/nl.json index dfc68671..aa5da731 100644 --- a/locales/nl.json +++ b/locales/nl.json @@ -14,7 +14,6 @@ "Clear watch history?": "Wil je de kijkgeschiedenis wissen?", "New password": "Nieuw wachtwoord", "New passwords must match": "De nieuwe wachtwoorden moeten overeenkomen", - "Cannot change password for Google accounts": "Kan het wachtwoord van Google-accounts niet wijzigen", "Authorize token?": "Wil je de toegangssleutel machtigen?", "Authorize token for `x`?": "Wil je de toegangssleutel machtigen voor `x`?", "Yes": "Ja", @@ -37,7 +36,6 @@ "source": "bron", "Log in": "Inloggen", "Log in/register": "Inloggen/Registreren", - "Log in with Google": "Inloggen met Google", "User ID": "Gebruikers-id", "Password": "Wachtwoord", "Time (h:mm:ss):": "Tijd (h:mm:ss):", @@ -46,7 +44,6 @@ "Sign In": "Inloggen", "Register": "Registreren", "E-mail": "E-mailadres", - "Google verification code": "Google-verificatiecode", "Preferences": "Instellingen", "preferences_category_player": "Spelerinstellingen", "preferences_video_loop_label": "Altijd herhalen: ", @@ -159,17 +156,12 @@ "Hide replies": "Antwoorden verbergen", "Show replies": "Antwoorden tonen", "Incorrect password": "Wachtwoord is onjuist", - "Quota exceeded, try again in a few hours": "Quota overschreden; probeer het over een paar uur opnieuw", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Kan niet inloggen. Zorg ervoor dat authenticatie in twee stappen (Authenticator of sms) is ingeschakeld.", - "Invalid TFA code": "Onjuiste TFA-code", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "Inloggen mislukt. Wellicht is authenticatie in twee stappen niet ingeschakeld op je account.", "Wrong answer": "Onjuist antwoord", "Erroneous CAPTCHA": "Onjuiste CAPTCHA", "CAPTCHA is a required field": "CAPTCHA is vereist", "User ID is a required field": "Gebruikers-id is vereist", "Password is a required field": "Wachtwoord is vereist", "Wrong username or password": "Onjuiste gebruikersnaam of wachtwoord", - "Please sign in using 'Log in with Google'": "Log in via 'Inloggen met Google'", "Password cannot be empty": "Het wachtwoordveld mag niet leeg zijn", "Password cannot be longer than 55 characters": "Het wachtwoord mag niet langer dan 55 tekens zijn", "Please log in": "Log in", diff --git a/locales/pl.json b/locales/pl.json index ca80757c..e237db8b 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -14,7 +14,6 @@ "Clear watch history?": "Wyczyścić historię?", "New password": "Nowe hasło", "New passwords must match": "Nowe hasła muszą być identyczne", - "Cannot change password for Google accounts": "Nie można zmienić hasła do konta Google", "Authorize token?": "Autoryzować token?", "Authorize token for `x`?": "Autoryzować token dla `x`?", "Yes": "Tak", @@ -37,7 +36,6 @@ "source": "źródło", "Log in": "Zaloguj", "Log in/register": "Zaloguj/Zarejestruj", - "Log in with Google": "Zaloguj do Google", "User ID": "ID użytkownika", "Password": "Hasło", "Time (h:mm:ss):": "Godzina (h:mm:ss):", @@ -46,7 +44,6 @@ "Sign In": "Zaloguj się", "Register": "Zarejestruj się", "E-mail": "E-mail", - "Google verification code": "Kod weryfikacyjny Google", "Preferences": "Preferencje", "preferences_category_player": "Ustawienia odtwarzacza", "preferences_video_loop_label": "Zawsze zapętlaj: ", @@ -163,17 +160,12 @@ "Hide replies": "Ukryj odpowiedzi", "Show replies": "Pokaż odpowiedzi", "Incorrect password": "Niepoprawne hasło", - "Quota exceeded, try again in a few hours": "Przekroczony limit zapytań, spróbuj ponownie za kilka godzin", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Nie udało się zalogować, upewnij się, że dwuetapowe uwierzytelnianie (Autentykator lub SMS) jest aktywne.", - "Invalid TFA code": "Niepoprawny kod TFA", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "Nie udało się zalogować. To może być spowodowane wyłączoną dwustopniową autoryzacją na twoim koncie.", "Wrong answer": "Niepoprawna odpowiedź", "Erroneous CAPTCHA": "CAPTCHA wykonane błędnie", "CAPTCHA is a required field": "CAPTCHA jest polem wymaganym", "User ID is a required field": "ID użytkownika jest polem wymaganym", "Password is a required field": "Hasło jest polem wymaganym", "Wrong username or password": "Niepoprawny login lub hasło", - "Please sign in using 'Log in with Google'": "Zaloguj się używając \"Zaloguj się przez Google\"", "Password cannot be empty": "Hasło nie może być puste", "Password cannot be longer than 55 characters": "Hasło nie może być dłuższe niż 55 znaków", "Please log in": "Proszę się zalogować", diff --git a/locales/pt-BR.json b/locales/pt-BR.json index 0a33e380..81290398 100644 --- a/locales/pt-BR.json +++ b/locales/pt-BR.json @@ -14,7 +14,6 @@ "Clear watch history?": "Limpar histórico de reprodução?", "New password": "Nova senha", "New passwords must match": "Nova senha deve ser igual", - "Cannot change password for Google accounts": "Não é possível alterar sua senha de contas do Google", "Authorize token?": "Autorizar o token?", "Authorize token for `x`?": "Autorizar o token para `x`?", "Yes": "Sim", @@ -37,7 +36,6 @@ "source": "código-fonte", "Log in": "Entrar", "Log in/register": "Entrar/Registrar", - "Log in with Google": "Entrar com conta Google", "User ID": "Usuário", "Password": "Senha", "Time (h:mm:ss):": "Hora (h:mm:ss):", @@ -46,7 +44,6 @@ "Sign In": "Entrar", "Register": "Registrar", "E-mail": "E-mail", - "Google verification code": "Código de verificação do Google", "Preferences": "Preferências", "preferences_category_player": "Preferências do reprodutor", "preferences_video_loop_label": "Repetir sempre: ", @@ -166,17 +163,12 @@ "Hide replies": "Ocultar respostas", "Show replies": "Mostrar respostas", "Incorrect password": "Senha incorreta", - "Quota exceeded, try again in a few hours": "Cota excedida, tente novamente em algumas horas", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Não foi possível fazer login, sua autenticação em dois passos (app autenticador ou sms) deve estar ativada.", - "Invalid TFA code": "Código TFA inválido", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "Falha no login. Isso pode acontecer porque a autenticação em dois passos está desativada para sua conta.", "Wrong answer": "Resposta incorreta", "Erroneous CAPTCHA": "CAPTCHA inválido", "CAPTCHA is a required field": "O CAPTCHA é um campo obrigatório", "User ID is a required field": "O nome de usuário é um campo obrigatório", "Password is a required field": "A senha é um campo obrigatório", "Wrong username or password": "Nome de usuário ou senha inválidos", - "Please sign in using 'Log in with Google'": "Por favor, entre usando 'Entrar com conta Google'", "Password cannot be empty": "A senha não pode ficar em branco", "Password cannot be longer than 55 characters": "A senha não pode ter mais que 55 caracteres", "Please log in": "Por favor, inicie sua sessão", diff --git a/locales/pt-PT.json b/locales/pt-PT.json index 43834d70..3834c9e2 100644 --- a/locales/pt-PT.json +++ b/locales/pt-PT.json @@ -14,7 +14,6 @@ "Clear watch history?": "Limpar histórico de reprodução?", "New password": "Nova palavra-chave", "New passwords must match": "As novas palavra-chaves devem corresponder", - "Cannot change password for Google accounts": "Não é possível alterar a palavra-chave para contas do Google", "Authorize token?": "Autorizar token?", "Authorize token for `x`?": "Autorizar token para `x`?", "Yes": "Sim", @@ -37,7 +36,6 @@ "source": "código-fonte", "Log in": "Iniciar sessão", "Log in/register": "Iniciar sessão/registar", - "Log in with Google": "Iniciar sessão com o Google", "User ID": "Utilizador", "Password": "Palavra-chave", "Time (h:mm:ss):": "Tempo (h:mm:ss):", @@ -46,7 +44,6 @@ "Sign In": "Iniciar sessão", "Register": "Registar", "E-mail": "E-mail", - "Google verification code": "Código de verificação do Google", "Preferences": "Preferências", "preferences_category_player": "Preferências do reprodutor", "preferences_video_loop_label": "Repetir sempre: ", @@ -166,17 +163,12 @@ "Hide replies": "Ocultar respostas", "Show replies": "Mostrar respostas", "Incorrect password": "Palavra-chave incorreta", - "Quota exceeded, try again in a few hours": "Cota excedida. Tente novamente dentro de algumas horas", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Não é possível iniciar a sessão, certifique-se que a autenticação de dois fatores (Autenticador ou SMS) está ativada.", - "Invalid TFA code": "Código TFA inválido", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "Falhou o início de sessão. Isto pode ser devido a não ter ativado na sua conta a autenticação de dois fatores (2FA).", "Wrong answer": "Resposta errada", "Erroneous CAPTCHA": "CAPTCHA inválido", "CAPTCHA is a required field": "CAPTCHA é um campo obrigatório", "User ID is a required field": "O nome de utilizador é um campo obrigatório", "Password is a required field": "Palavra-chave é um campo obrigatório", "Wrong username or password": "Nome de utilizador ou palavra-chave incorreto", - "Please sign in using 'Log in with Google'": "Por favor, inicie sessão usando 'Iniciar sessão com o Google'", "Password cannot be empty": "A palavra-chave não pode estar vazia", "Password cannot be longer than 55 characters": "A palavra-chave não pode ser superior a 55 caracteres", "Please log in": "Por favor, inicie sessão", diff --git a/locales/pt.json b/locales/pt.json index cbce0e5a..c817460a 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -63,8 +63,6 @@ "Could not pull trending pages.": "Não foi possível obter as páginas de tendências.", "Could not create mix.": "Não foi possível criar a mistura.", "Deleted or invalid channel": "Canal eliminado ou inválido", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "Falhou o início de sessão. Isto pode ser devido a não ter ativado na sua conta a autenticação de dois fatores (2FA).", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Não é possível iniciar a sessão, certifique-se que a autenticação de dois fatores (Autenticador ou SMS) está ativada.", "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Olá! Parece que o JavaScript está desativado. Clique aqui para ver os comentários, entretanto eles podem levar mais tempo para carregar.", "Delete playlist": "Eliminar lista de reprodução", "Delete playlist `x`?": "Eliminar a lista de reprodução 'x'?", @@ -81,7 +79,6 @@ "Log in/register": "Iniciar sessão/registar", "Delete account?": "Eliminar conta?", "Import and Export Data": "Importar e exportar dados", - "Cannot change password for Google accounts": "Não é possível alterar a palavra-chave para contas do Google", "Filipino": "Filipino", "Estonian": "Estónio", "Esperanto": "Esperanto", @@ -125,15 +122,12 @@ "Please log in": "Por favor, inicie sessão", "Password cannot be longer than 55 characters": "A palavra-chave não pode ser superior a 55 caracteres", "Password cannot be empty": "A palavra-chave não pode estar vazia", - "Please sign in using 'Log in with Google'": "Por favor, inicie sessão usando 'Iniciar sessão com o Google'", "Wrong username or password": "Nome de utilizador ou palavra-chave incorreto", "Password is a required field": "Palavra-chave é um campo obrigatório", "User ID is a required field": "O nome de utilizador é um campo obrigatório", "CAPTCHA is a required field": "CAPTCHA é um campo obrigatório", "Erroneous CAPTCHA": "CAPTCHA inválido", "Wrong answer": "Resposta errada", - "Invalid TFA code": "Código TFA inválido", - "Quota exceeded, try again in a few hours": "Cota excedida. Tente novamente dentro de algumas horas", "Incorrect password": "Palavra-chave incorreta", "Show replies": "Mostrar respostas", "Hide replies": "Ocultar respostas", @@ -232,7 +226,6 @@ "preferences_video_loop_label": "Repetir sempre: ", "preferences_category_player": "Preferências do reprodutor", "Preferences": "Preferências", - "Google verification code": "Código de verificação do Google", "E-mail": "E-mail", "Register": "Registar", "Image CAPTCHA": "Imagem CAPTCHA", @@ -240,7 +233,6 @@ "Time (h:mm:ss):": "Tempo (h:mm:ss):", "Password": "Palavra-chave", "User ID": "Utilizador", - "Log in with Google": "Iniciar sessão com o Google", "Log in": "Iniciar sessão", "source": "código-fonte", "JavaScript license information": "Informação de licença do JavaScript", diff --git a/locales/ro.json b/locales/ro.json index 0f6407d6..85bf746f 100644 --- a/locales/ro.json +++ b/locales/ro.json @@ -14,7 +14,6 @@ "Clear watch history?": "Doriți să ștergeți istoricul?", "New password": "Parola nouă", "New passwords must match": "Câmpurile \"Parolă nouă\" trebuie să fie identice", - "Cannot change password for Google accounts": "Parola pentru un cont Google nu poate fi schimbată de pe Invidious", "Authorize token?": "Autorizați token-ul?", "Authorize token for `x`?": "Autorizați token-ul pentru `x` ?", "Yes": "Da", @@ -37,7 +36,6 @@ "source": "sursă", "Log in": "Conectați-vă", "Log in/register": "Conectați-vă/Creați-vă un cont", - "Log in with Google": "Conectați-vă cu Google", "User ID": "ID Utilizator", "Password": "Parolă", "Time (h:mm:ss):": "Ora (h:mm:ss) :", @@ -46,7 +44,6 @@ "Sign In": "Conectați-vă", "Register": "Înregistrați-vă", "E-mail": "E-mail", - "Google verification code": "Cod de verificare Google", "Preferences": "Preferințe", "preferences_category_player": "Setări de redare", "preferences_video_loop_label": "Reluați videoclipul la nesfârșit: ", @@ -155,17 +152,12 @@ "Hide replies": "Ascundeți replicile", "Show replies": "Afișați replicile", "Incorrect password": "Parolă incorectă", - "Quota exceeded, try again in a few hours": "Numărul de tentative de conectare a fost depășit. Va rugăm să încercați din nou în câteva ore", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Conectare eșuată. Dacă nu reușiți să vă conectați, verificați dacă ați activat autentificarea cu doi factori (Autentificator sau SMS).", - "Invalid TFA code": "Codul de autentificare cu doi factori este invalid", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "Conectare eșuată. Acest lucru ar putea fi cauzat de faptul că nu ați activat autentificarea cu doi factori.", "Wrong answer": "Răspuns invalid", "Erroneous CAPTCHA": "CAPTCHA invalid", "CAPTCHA is a required field": "Câmpul CAPTCHA este obligatoriu", "User ID is a required field": "Câmpul ID Utilizator este obligatoriu", "Password is a required field": "Câmpul Parolă este obligatoriu", "Wrong username or password": "Nume de utilizator sau parolă invalidă", - "Please sign in using 'Log in with Google'": "Vă rog conectați-vă folosind \"Conectați-vă cu Google\"", "Password cannot be empty": "Parola nu poate fi goală", "Password cannot be longer than 55 characters": "Parola nu poate să conțină mai mult de 55 de caractere", "Please log in": "Vă rog conectați-vă", diff --git a/locales/ru.json b/locales/ru.json index 5907d567..7f79a90c 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -14,7 +14,6 @@ "Clear watch history?": "Очистить историю просмотров?", "New password": "Новый пароль", "New passwords must match": "Новые пароли не совпадают", - "Cannot change password for Google accounts": "Изменить пароль учётной записи Google невозможно", "Authorize token?": "Авторизовать токен?", "Authorize token for `x`?": "Авторизовать токен для `x`?", "Yes": "Да", @@ -37,7 +36,6 @@ "source": "источник", "Log in": "Войти", "Log in/register": "Войти или зарегистрироваться", - "Log in with Google": "Войти через Google", "User ID": "ИД пользователя", "Password": "Пароль", "Time (h:mm:ss):": "Время (ч:мм:сс):", @@ -46,7 +44,6 @@ "Sign In": "Войти", "Register": "Зарегистрироваться", "E-mail": "Эл. почта", - "Google verification code": "Код подтверждения Google", "Preferences": "Настройки", "preferences_category_player": "Настройки проигрывателя", "preferences_video_loop_label": "Всегда повторять: ", @@ -164,17 +161,12 @@ "Hide replies": "Скрыть ответы", "Show replies": "Показать ответы", "Incorrect password": "Неправильный пароль", - "Quota exceeded, try again in a few hours": "Лимит превышен, попробуйте снова через несколько часов", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Не удалось войти. Проверьте, не включена ли двухфакторная аутентификация (по коду или смс).", - "Invalid TFA code": "Неправильный код двухфакторной аутентификации", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "Не удалось войти. Это может быть из-за того, что в вашем аккаунте не включена двухфакторная аутентификация.", "Wrong answer": "Неправильный ответ", "Erroneous CAPTCHA": "Неправильная капча", "CAPTCHA is a required field": "Необходимо решить капчу", "User ID is a required field": "Необходимо ввести идентификатор пользователя", "Password is a required field": "Необходимо ввести пароль", "Wrong username or password": "Неправильный логин или пароль", - "Please sign in using 'Log in with Google'": "Пожалуйста, нажмите «Войти через Google»", "Password cannot be empty": "Пароль не может быть пустым", "Password cannot be longer than 55 characters": "Пароль не может быть длиннее 55 символов", "Please log in": "Пожалуйста, войдите", diff --git a/locales/si.json b/locales/si.json index 69501343..19f34fac 100644 --- a/locales/si.json +++ b/locales/si.json @@ -14,7 +14,6 @@ "oldest": "පැරණිතම", "popular": "ජනප්‍රිය", "last": "අවසන්", - "Cannot change password for Google accounts": "Google ගිණුම් සඳහා මුරපදය වෙනස් කළ නොහැක", "Authorize token?": "ටෝකනය අනුමත කරනවා ද?", "Authorize token for `x`?": "`x` සඳහා ටෝකනය අනුමත කරනවා ද?", "Yes": "ඔව්", @@ -31,7 +30,6 @@ "An alternative front-end to YouTube": "YouTube සඳහා විකල්ප ඉදිරිපස අන්තයක්", "source": "මූලාශ්‍රය", "Log in/register": "පුරන්න/ලියාපදිංචිවන්න", - "Log in with Google": "Google සමඟ පුරන්න", "Password": "මුරපදය", "Time (h:mm:ss):": "වේලාව (h:mm:ss):", "Sign In": "පුරන්න", @@ -86,7 +84,6 @@ "User ID": "පරිශීලක කේතය", "Text CAPTCHA": "CAPTCHA පෙල", "Image CAPTCHA": "CAPTCHA රූපය", - "Google verification code": "Google සත්‍යාපන කේතය", "E-mail": "විද්‍යුත් තැපෑල", "preferences_quality_label": "කැමති වීඩියෝ ගුණත්වය: ", "preferences_quality_option_hd720": "HD720", diff --git a/locales/sk.json b/locales/sk.json index cdb3a596..7346dc58 100644 --- a/locales/sk.json +++ b/locales/sk.json @@ -12,7 +12,6 @@ "Clear watch history?": "Vymazať históriu sledovania?", "New password": "Nové heslo", "New passwords must match": "Nové heslá sa musia zhodovať", - "Cannot change password for Google accounts": "Heslo pre účty Google sa nedá zmeniť", "Authorize token?": "Autorizovať token?", "Yes": "Áno", "No": "Nie", @@ -34,7 +33,6 @@ "source": "zdroj", "Log in": "Prihlásiť sa", "Log in/register": "Prihlásiť sa/Registrovať", - "Log in with Google": "Prihlásiť sa pomocou účtu Google", "User ID": "ID používateľa", "Password": "Heslo", "Time (h:mm:ss):": "Čas (h:mm:ss):", @@ -43,7 +41,6 @@ "Sign In": "Prihlásiť sa", "Register": "Registrovať", "E-mail": "E-mail", - "Google verification code": "Overovací kód Google", "Preferences": "Nastavenia", "preferences_category_player": "Nastavenia prehrávača", "preferences_video_loop_label": "Vždy opakovať: ", diff --git a/locales/sl.json b/locales/sl.json index 410b432c..592ba78f 100644 --- a/locales/sl.json +++ b/locales/sl.json @@ -8,7 +8,6 @@ "Clear watch history?": "Izbrisati zgodovino ogledov?", "New password": "Novo geslo", "New passwords must match": "Nova gesla se morajo ujemati", - "Cannot change password for Google accounts": "Ni mogoče spremeniti gesla za račune Google", "Authorize token?": "Naj odobrim žeton?", "Yes": "Da", "Import and Export Data": "Uvoz in izvoz podatkov", @@ -22,7 +21,6 @@ "Export subscriptions as OPML (for NewPipe & FreeTube)": "Izvozi naročnine kot OPML (za NewPipe in FreeTube)", "Log in": "Prijava", "Log in/register": "Prijava/registracija", - "Log in with Google": "Prijavi se z Googlom", "User ID": "ID uporabnika", "Password": "Geslo", "Time (h:mm:ss):": "Čas (h:mm:ss):", @@ -32,7 +30,6 @@ "Sign In": "Prijavi se", "Register": "Registriraj se", "E-mail": "E-pošta", - "Google verification code": "Googlova koda za preverjanje", "Preferences": "Nastavitve", "preferences_video_loop_label": "Vedno v zanki: ", "preferences_autoplay_label": "Samodejno predvajanje: ", @@ -120,9 +117,6 @@ "([^.,0-9]|^)1([^.,0-9]|$)": "Poglej `x` komentar", "": "Poglej `x` komentarjev" }, - "Quota exceeded, try again in a few hours": "Kvota je presežena, poskusi znova čez nekaj ur", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Ne morem se prijaviti, preveri, ali je vklopljeno dvofaktorsko preverjanje pristnosti (avtentikator ali SMS).", - "Please sign in using 'Log in with Google'": "Prijavi se z uporabo »Prijava z Googlom«", "Password cannot be empty": "Geslo ne sme biti prazno", "`x` ago": "`x` nazaj", "Load more": "Naloži več", @@ -348,8 +342,6 @@ "View Reddit comments": "Oglej si komentarje na Redditu", "This channel does not exist.": "Ta kanal ne obstaja.", "Hide replies": "Skrij odgovore", - "Invalid TFA code": "Neveljavna koda TFA", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "Prijava ni uspela. To je lahko zato, ker za tvoj račun ni vklopljeno dvofaktorsko preverjanje pristnosti.", "Invidious Private Feed for `x`": "Invidious zasebni vir za `x`", "Deleted or invalid channel": "Izbrisan ali neveljaven kanal", "Empty playlist": "Prazen seznam predvajanja", diff --git a/locales/sq.json b/locales/sq.json index 7f29a035..d28eb784 100644 --- a/locales/sq.json +++ b/locales/sq.json @@ -35,12 +35,10 @@ "videoinfo_youTube_embed_link": "Trupëzojeni", "videoinfo_invidious_embed_link": "Lidhje Trupëzimi", "oldest": "më të vjetrat", - "Cannot change password for Google accounts": "S’mund të ndryshojë fjalëkalimin për llogari Google", "New passwords must match": "Fjalëkalimet e rinj duhet të përputhen me njëri-tjetrin", "Authorize token?": "Të autorizohet token-i?", "Authorize token for `x`?": "Të autorizohet token-i për `x`?", "Log in/register": "Hyni/regjistrohuni", - "Log in with Google": "Hyni me Google", "User ID": "ID Përdoruesi", "Password": "Fjalëkalim", "Time (h:mm:ss):": "Kohë (h:mm:ss):", @@ -156,19 +154,14 @@ "Whitelisted regions: ": "Rajone të lejuara: ", "Premieres `x`": "Premiera `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.": "Njatjeta! Duket sikur keni JavaScript-in të çaktivizuar. Klikoni këtu që të shihni komentet, mbani parasysh se mund të duhet pak më tepër kohë që të ngarkohen.", - "Quota exceeded, try again in a few hours": "Janë tejkaluar kuotat, riprovoni pas pak orësh", "Blacklisted regions: ": "Rajone të palejuara: ", "Premieres in `x`": "Premiera në `x`", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "S’arrihet të bëhet hyrja, sigurohuni se mirëfilltësimi dyfaktorësh (me Mirëfilltësues apo SMS) është i aktivizuar.", "Wrong answer": "Përgjigje e gabuar", - "Invalid TFA code": "Kod MDF i pavlefshëm", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "Dështoi hyrja. Kjo mund të vijë ngaqë për llogarinë tuaj s’është aktivizuar mirëfilltësimi dyfaktorësh.", "Erroneous CAPTCHA": "CAPTCHA e gabuar", "CAPTCHA is a required field": "CAPTCHA është fushë e domosdoshme", "User ID is a required field": "ID-ja e përdoruesit është fushë e domosdoshme", "Password is a required field": "Fusha e fjalëkalimit është e domosdoshme", "Wrong username or password": "Emër përdoruesi ose fjalëkalim i gabuar", - "Please sign in using 'Log in with Google'": "Ju lutemi, bëni hyrjen duke përdorur “Bëni hyrjen me Google”", "Password cannot be empty": "Fjalëkalimi s’mund të jetë i zbrazët", "Password cannot be longer than 55 characters": "Fjalëkalimi s’mund të jetë më i gjatë se 55 shenja", "Please log in": "Ju lutemi, bëni hyrjen", @@ -303,7 +296,6 @@ "Previous page": "Faqja e mëparshme", "Clear watch history?": "Të spastrohet historiku i parjeve?", "New password": "Fjalëkalim i ri", - "Google verification code": "Kod verifikimi Google", "preferences_related_videos_label": "Shfaq video të afërta: ", "preferences_annotations_label": "Si parazgjedhje, shfaqi shënimet: ", "preferences_show_nick_label": "Shfaqe nofkën në krye: ", diff --git a/locales/sr.json b/locales/sr.json index fd19c493..a2853b68 100644 --- a/locales/sr.json +++ b/locales/sr.json @@ -14,7 +14,6 @@ "Clear watch history?": "Izbrisati povest pregledanja?", "New password": "Nova lozinka", "New passwords must match": "Nove lozinke moraju biti istovetne", - "Cannot change password for Google accounts": "Nije moguće promeniti lozinku za Google naloge", "Authorize token?": "Ovlasti žeton?", "Authorize token for `x`?": "Ovlasti žeton za `x`?", "Yes": "Da", @@ -37,7 +36,6 @@ "source": "izvor", "Log in": "Prijavi se", "Log in/register": "Prijavi se/Otvori nalog", - "Log in with Google": "Prijavi se pomoću Google-a", "User ID": "Korisnički ID", "Password": "Lozinka", "Time (h:mm:ss):": "Vreme (č:mm:ss):", @@ -46,7 +44,6 @@ "Sign In": "Prijava", "Register": "Otvori nalog", "E-mail": "E-pošta", - "Google verification code": "Google-ova overna koda", "Preferences": "Podešavanja", "preferences_category_player": "Podešavanja reproduktora", "preferences_video_loop_label": "Uvek ponavljaj: ", @@ -57,13 +54,11 @@ "preferences_local_label": "Prikaz video zapisa preko posrednika: ", "Playlist privacy": "Podešavanja privatnosti plej liste", "Editing playlist `x`": "Izmena plej liste `x`", - "Please sign in using 'Log in with Google'": "Molimo Vas da se prijavite pomoću 'Log in with Google'", "Playlist does not exist.": "Nepostojeća plej lista.", "Erroneous challenge": "Pogrešan izazov", "Maltese": "Malteški", "Download": "Preuzmi", "Download as: ": "Preuzmi kao: ", - "Quota exceeded, try again in a few hours": "Kvota je premašena, molimo vas da pokušate ponovo za par sati", "Bangla": "Bangla/Bengalski", "preferences_quality_dash_label": "Preferirani kvalitet DASH video formata: ", "Token manager": "Upravljanje žetonima", @@ -182,7 +177,6 @@ "": "Prikaži `x` komentara" }, "View Reddit comments": "Prikaži Reddit komentare", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Neuspešna prijava, proverite da li ste upalili dvofaktornu autentikaciju (Autentikator ili SMS).", "CAPTCHA is a required field": "CAPTCHA je obavezno polje", "Croatian": "Hrvatski", "Estonian": "Estonski", @@ -283,8 +277,6 @@ "Wrong answer": "Pogrešan odgovor", "preferences_quality_label": "Preferirani video kvalitet: ", "Hide replies": "Sakrij odgovore", - "Invalid TFA code": "Nevažeća TFA koda", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "Neuspešna prijava! Ovo se možda dešava jer dvofaktorna autentikacija nije omogućena na vašem nalogu.", "Erroneous CAPTCHA": "Pogrešna CAPTCHA", "Erroneous token": "Pogrešan žeton", "Czech": "Češki", diff --git a/locales/sr_Cyrl.json b/locales/sr_Cyrl.json index bef9915d..218f31c9 100644 --- a/locales/sr_Cyrl.json +++ b/locales/sr_Cyrl.json @@ -14,7 +14,6 @@ "Clear watch history?": "Избрисати повест прегледања?", "New password": "Нова лозинка", "New passwords must match": "Нове лозинке морају бити истоветне", - "Cannot change password for Google accounts": "Није могуће променити лозинку за Google налоге", "Authorize token?": "Овласти жетон?", "Authorize token for `x`?": "Овласти жетон за `x`?", "Yes": "Да", @@ -37,7 +36,6 @@ "source": "извор", "Log in": "Пријави се", "Log in/register": "Пријави се/Отворите налог", - "Log in with Google": "Пријави се помоћу Google-а", "User ID": "Кориснички ИД", "Password": "Лозинка", "Time (h:mm:ss):": "Време (ч:мм:сс):", @@ -46,7 +44,6 @@ "Sign In": "Пријава", "Register": "Отвори налог", "E-mail": "Е-пошта", - "Google verification code": "Google-ова оверна кода", "Preferences": "Подешавања", "preferences_category_player": "Подешавања репродуктора", "preferences_video_loop_label": "Увек понављај: ", @@ -150,8 +147,6 @@ "Burmese": "Бурмански", "preferences_quality_dash_label": "Преферирани квалитет DASH видео формата: ", "Erroneous token": "Погрешан жетон", - "Quota exceeded, try again in a few hours": "Квота је премашена, молимо вас да покушате поново за пар сати", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Неуспешна пријава, проверите да ли сте упалили двофакторну аутентикацију (Аутентикатор или СМС).", "CAPTCHA is a required field": "CAPTCHA је обавезно поље", "No such user": "Непостојећи корисник", "Chinese (Traditional)": "Кинески (Традиционални)", @@ -164,7 +159,6 @@ "preferences_show_nick_label": "Прикажи надимке на врху: ", "Report statistics: ": "Извештавај о статистици: ", "Show more": "Прикажи више", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "Неуспешна пријава! Ово се можда дешава јер двофакторна аутентикација није омогућена на vашем налогу.", "Wrong answer": "Погрешан одговор", "Hidden field \"token\" is a required field": "Сакривено \"token\" поље је обавезно", "English": "Енглески", @@ -198,7 +192,6 @@ "User ID is a required field": "Кориснички ИД је обавезно поље", "Password is a required field": "Лозинка је обавезно поље", "Wrong username or password": "Погрешно корисничко име или лозинка", - "Please sign in using 'Log in with Google'": "Молимо Вас да се пријавите помоћу 'Log in with Google'", "Password cannot be empty": "Лозинка не може бити празна", "Password cannot be longer than 55 characters": "Лозинка не може бити дужа од 55 карактера", "Invidious Private Feed for `x`": "Инвидиоус Приватни Довод за `x`", @@ -324,7 +317,6 @@ "Released under the AGPLv3 on Github.": "Избачено под лиценцом AGPLv3 на GitHub-у.", "Afrikaans": "Африканс", "preferences_automatic_instance_redirect_label": "Аутоматско пребацивање на другу инстанцу у случају отказивања (пречи ће назад на редирецт.инвидиоус.ио): ", - "Invalid TFA code": "Неважећа TFA кода", "Please log in": "Молимо вас да се пријавите", "English (auto-generated)": "Енглески (аутоматски генерисано)", "Hindi": "Хинди", diff --git a/locales/sv-SE.json b/locales/sv-SE.json index 39e94fd3..a319fffd 100644 --- a/locales/sv-SE.json +++ b/locales/sv-SE.json @@ -14,7 +14,6 @@ "Clear watch history?": "Töm visningshistorik?", "New password": "Nytt lösenord", "New passwords must match": "Nya lösenord måste stämma överens", - "Cannot change password for Google accounts": "Kan inte ändra lösenord på Google-konton", "Authorize token?": "Auktorisera åtkomsttoken?", "Authorize token for `x`?": "Auktorisera åtkomsttoken för `x`?", "Yes": "Ja", @@ -37,7 +36,6 @@ "source": "källa", "Log in": "Logga in", "Log in/register": "Logga in/registrera", - "Log in with Google": "Logga in med Google", "User ID": "Användar-ID", "Password": "Lösenord", "Time (h:mm:ss):": "Tid (h:mm:ss):", @@ -46,7 +44,6 @@ "Sign In": "Inloggning", "Register": "Registrera", "E-mail": "E-post", - "Google verification code": "Google-bekräftelsekod", "Preferences": "Inställningar", "preferences_category_player": "Spelarinställningar", "preferences_video_loop_label": "Loopa alltid: ", @@ -162,17 +159,12 @@ "Hide replies": "Dölj svar", "Show replies": "Visa svar", "Incorrect password": "Fel lösenord", - "Quota exceeded, try again in a few hours": "Kvoten överskriden, försök igen om ett par timmar", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Kunde inte logga in, försäkra dig om att tvåfaktors-autentisering (Authenticator eller SMS) är påslagen.", - "Invalid TFA code": "Ogiltig tvåfaktor-kod", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "Inloggning misslyckades. Detta kan vara för att tvåfaktors-autentisering inte är påslaget på ditt konto.", "Wrong answer": "Fel svar", "Erroneous CAPTCHA": "Ogiltig CAPTCHA", "CAPTCHA is a required field": "CAPTCHA är ett obligatoriskt fält", "User ID is a required field": "Användar-ID är ett obligatoriskt fält", "Password is a required field": "Lösenord är ett obligatoriskt fält", "Wrong username or password": "Ogiltigt användarnamn eller lösenord", - "Please sign in using 'Log in with Google'": "Logga in genom \"Google-inloggning\"", "Password cannot be empty": "Lösenordet kan inte vara tomt", "Password cannot be longer than 55 characters": "Lösenordet kan inte vara längre än 55 tecken", "Please log in": "Logga in", diff --git a/locales/tr.json b/locales/tr.json index ca74ef23..22732a51 100644 --- a/locales/tr.json +++ b/locales/tr.json @@ -14,7 +14,6 @@ "Clear watch history?": "İzleme geçmişi temizlensin mi?", "New password": "Yeni Parola", "New passwords must match": "Yeni Parolalar Eşleşmek Zorunda", - "Cannot change password for Google accounts": "Google Hesapları İçin Parola Değiştirilemez", "Authorize token?": "Belirteç yetkilendirilsin mi?", "Authorize token for `x`?": "`x` için belirteç yetkilendirilsin mi?", "Yes": "Evet", @@ -37,7 +36,6 @@ "source": "Kaynak", "Log in": "Oturum Aç", "Log in/register": "Oturum Aç/Kayıt Ol", - "Log in with Google": "Google İle Oturum Aç", "User ID": "Kullanıcı Kimliği", "Password": "Parola", "Time (h:mm:ss):": "Zaman (h:mm:ss):", @@ -46,7 +44,6 @@ "Sign In": "Oturum Aç", "Register": "Kayıt Ol", "E-mail": "E-Posta", - "Google verification code": "Google Doğrulama Kodu", "Preferences": "Tercihler", "preferences_category_player": "Oynatıcı Tercihleri", "preferences_video_loop_label": "Sürekli Döngü: ", @@ -164,17 +161,12 @@ "Hide replies": "Cevapları Gizle", "Show replies": "Cevapları Göster", "Incorrect password": "Yanlış Parola", - "Quota exceeded, try again in a few hours": "Kota aşıldı, birkaç saat içinde tekrar deneyin.", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Oturum açılamadı, iki faktörlü kimlik doğrulamanın (Kimlik Doğrulayıcı ya da SMS) açık olduğundan emin olun.", - "Invalid TFA code": "Geçersiz TFA Kodu", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "Giriş başarısız. Bunun nedeni, hesabınız için iki faktörlü kimlik doğrulamanın açık olmaması olabilir.", "Wrong answer": "Yanlış Cevap", "Erroneous CAPTCHA": "Hatalı CAPTCHA", "CAPTCHA is a required field": "CAPTCHA Zorunlu Bir Alandır", "User ID is a required field": "Kullanıcı Kimliği Zorunlu Bir Alandır", "Password is a required field": "Parola Zorunlu Bir Alandır", "Wrong username or password": "Yanlış Kullanıcı Adı ya da Parola", - "Please sign in using 'Log in with Google'": "Lütfen 'Google İle Giriş Yap' Seçeneğini Kullanarak Oturum Açın", "Password cannot be empty": "Parola Boş Olamaz", "Password cannot be longer than 55 characters": "Parola 55 Karakterden Uzun Olamaz", "Please log in": "Lütfen Oturum Açın", diff --git a/locales/uk.json b/locales/uk.json index 863916f7..308b10ca 100644 --- a/locales/uk.json +++ b/locales/uk.json @@ -14,7 +14,6 @@ "Clear watch history?": "Очистити історію переглядів?", "New password": "Новий пароль", "New passwords must match": "Нові паролі не співпадають", - "Cannot change password for Google accounts": "Змінити пароль обліківки Google неможливо", "Authorize token?": "Авторизувати токен?", "Authorize token for `x`?": "Авторизувати токен для `x`?", "Yes": "Так", @@ -37,7 +36,6 @@ "source": "джерело", "Log in": "Увійти", "Log in/register": "Увійти або зареєструватися", - "Log in with Google": "Увійти через Google", "User ID": "ID користувача", "Password": "Пароль", "Time (h:mm:ss):": "Час (г:хх:сс):", @@ -46,7 +44,6 @@ "Sign In": "Увійти", "Register": "Зареєструватися", "E-mail": "Електронна пошта", - "Google verification code": "Код підтвердження Google", "Preferences": "Налаштування", "preferences_category_player": "Налаштування програвача", "preferences_video_loop_label": "Завжди повторювати: ", @@ -155,17 +152,12 @@ "Hide replies": "Сховати відповіді", "Show replies": "Показати відповіді", "Incorrect password": "Неправильний пароль", - "Quota exceeded, try again in a few hours": "Ліміт перевищено, спробуйте знову за декілька годин", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Не вдається увійти. Перевірте, чи не ввімкнена двофакторна аутентифікація (за кодом чи смс).", - "Invalid TFA code": "Неправильний код двофакторної автентифікації", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "Не вдається увійти. Це може бути через те, що у вашій обліківці не ввімкнена двофакторна аутентифікація.", "Wrong answer": "Неправильна відповідь", "Erroneous CAPTCHA": "Неправильна капча", "CAPTCHA is a required field": "Необхідно пройти CAPTCHA", "User ID is a required field": "Необхідно ввести ID користувача", "Password is a required field": "Необхідно ввести пароль", "Wrong username or password": "Неправильний логін чи пароль", - "Please sign in using 'Log in with Google'": "Будь ласка, натисніть «Увійти через Google»", "Password cannot be empty": "Пароль не може бути порожнім", "Password cannot be longer than 55 characters": "Пароль не може бути довшим за 55 знаків", "Please log in": "Будь ласка, увійдіть", diff --git a/locales/vi.json b/locales/vi.json index 3f7125c4..42076745 100644 --- a/locales/vi.json +++ b/locales/vi.json @@ -16,7 +16,6 @@ "Clear watch history?": "Xóa lịch sử xem?", "New password": "Mật khẩu mới", "New passwords must match": "Mật khẩu mới phải khớp", - "Cannot change password for Google accounts": "Không thể thay đổi mật khẩu cho tài khoản Google", "Authorize token?": "Cấp phép mã thông báo?", "Authorize token for `x`?": "Cấp phép mã thông báo cho` x`?", "Yes": "Đúng", @@ -39,7 +38,6 @@ "source": "nguồn", "Log in": "Đăng nhập", "Log in/register": "Đăng nhập / đăng ký", - "Log in with Google": "Đăng nhập bằng Google", "User ID": "Tên người dùng", "Password": "Mật khẩu", "Time (h:mm:ss):": "Thời gian (h: mm: ss):", @@ -48,7 +46,6 @@ "Sign In": "Đăng nhập", "Register": "Đăng ký", "E-mail": "E-mail", - "Google verification code": "Mã xác minh của Google", "Preferences": "Sở thích", "preferences_category_player": "Tùy chọn người chơi", "preferences_video_loop_label": "Luôn lặp lại: ", @@ -152,17 +149,12 @@ "Hide replies": "Ẩn câu trả lời", "Show replies": "Hiển thị câu trả lời", "Incorrect password": "Mật khẩu không đúng", - "Quota exceeded, try again in a few hours": "Đã vượt quá hạn ngạch, hãy thử lại sau vài giờ nữa", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Không thể đăng nhập, hãy đảm bảo rằng xác thực hai yếu tố (Authenticator hoặc SMS) được bật.", - "Invalid TFA code": "Mã TFA không hợp lệ", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "Đăng nhập không thành công. Điều này có thể là do xác thực hai yếu tố chưa được bật cho tài khoản của bạn.", "Wrong answer": "Câu trả lời sai", "Erroneous CAPTCHA": "CAPTCHA bị lỗi", "CAPTCHA is a required field": "CAPTCHA là trường bắt buộc", "User ID is a required field": "User ID là trường bắt buộc", "Password is a required field": "Mật khẩu là trường bắt buộc", "Wrong username or password": "Tên người dùng hoặc mật khẩu sai", - "Please sign in using 'Log in with Google'": "Vui lòng đăng nhập bằng 'Đăng nhập bằng Google'", "Password cannot be empty": "Mật khẩu không được để trống", "Password cannot be longer than 55 characters": "Mật khẩu không được dài hơn 55 ký tự", "Please log in": "Xin vui lòng đăng nhập", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index fdd940c3..58b834fa 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -19,7 +19,6 @@ "Clear watch history?": "清除观看历史?", "New password": "新密码", "New passwords must match": "新密码必须匹配", - "Cannot change password for Google accounts": "无法为 Google 账户更改密码", "Authorize token?": "授权令牌?", "Authorize token for `x`?": "`x` 的授权令牌?", "Yes": "是", @@ -42,7 +41,6 @@ "source": "source", "Log in": "登录", "Log in/register": "登录/注册", - "Log in with Google": "使用 Google 账户登录", "User ID": "用户 ID", "Password": "密码", "Time (h:mm:ss):": "时间 (h:mm:ss):", @@ -51,7 +49,6 @@ "Sign In": "登录", "Register": "注册", "E-mail": "E-mail", - "Google verification code": "Google 验证代码", "Preferences": "偏好设置", "preferences_category_player": "播放器偏好设置", "preferences_video_loop_label": "始终循环: ", @@ -171,17 +168,12 @@ "Hide replies": "隐藏回复", "Show replies": "显示回复", "Incorrect password": "密码错误", - "Quota exceeded, try again in a few hours": "已超出限额,请于几小时后重试", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "无法登录。请确认你的短信或验证器的二步验证已打开。", - "Invalid TFA code": "无效的二步验证码", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "登录失败。可能是因为二步验证未打开。", "Wrong answer": "错误的回复", "Erroneous CAPTCHA": "验证码错误", "CAPTCHA is a required field": "验证码必填", "User ID is a required field": "用户名必填", "Password is a required field": "密码必填", "Wrong username or password": "用户名或密码错误", - "Please sign in using 'Log in with Google'": "请通过谷歌账户登录", "Password cannot be empty": "密码不能为空", "Password cannot be longer than 55 characters": "密码长度不能大于 55", "Please log in": "请登录", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 593a946a..7da2d762 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -19,7 +19,6 @@ "Clear watch history?": "清除觀看歷史?", "New password": "新密碼", "New passwords must match": "新密碼必須符合", - "Cannot change password for Google accounts": "無法變更 Google 帳號的密碼", "Authorize token?": "授權 token?", "Authorize token for `x`?": "`x` 的授權 token?", "Yes": "是", @@ -42,7 +41,6 @@ "source": "來源", "Log in": "登入", "Log in/register": "登入/註冊", - "Log in with Google": "使用 Google 登入", "User ID": "使用者 ID", "Password": "密碼", "Time (h:mm:ss):": "時間 (h:mm:ss):", @@ -51,7 +49,6 @@ "Sign In": "登入", "Register": "註冊", "E-mail": "電子郵件", - "Google verification code": "Google 驗證碼", "Preferences": "偏好設定", "preferences_category_player": "播放器偏好設定", "preferences_video_loop_label": "總是循環播放: ", @@ -171,17 +168,12 @@ "Hide replies": "隱藏回覆", "Show replies": "顯示回覆", "Incorrect password": "不正確的密碼", - "Quota exceeded, try again in a few hours": "超過限額,請在幾個小時後再試一次", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "無法登入,請確定雙因素驗證(驗證器或簡訊)已開啟。", - "Invalid TFA code": "無效的 TFA 代碼", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "登入失敗。這可能是因為您的帳號未開啟雙因素驗證的關係。", "Wrong answer": "錯誤的答案", "Erroneous CAPTCHA": "錯誤的 CAPTCHA", "CAPTCHA is a required field": "CAPTCHA 為必填欄位", "User ID is a required field": "使用者 ID 為必填欄位", "Password is a required field": "密碼為必填欄位", "Wrong username or password": "錯誤的使用者名稱或密碼", - "Please sign in using 'Log in with Google'": "請使用「以 Google 登入」來登入", "Password cannot be empty": "密碼不能為空", "Password cannot be longer than 55 characters": "密碼不能長於55個字元", "Please log in": "請登入", From 62bd895562bfe91a402554d567adc4316ee6d1be Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 10 Jun 2023 17:37:44 +0200 Subject: [PATCH 247/598] User: Remove broken Google login (HTML form) --- src/invidious/views/user/login.ecr | 36 ------------------------------ 1 file changed, 36 deletions(-) diff --git a/src/invidious/views/user/login.ecr b/src/invidious/views/user/login.ecr index 01d7a210..2b03d280 100644 --- a/src/invidious/views/user/login.ecr +++ b/src/invidious/views/user/login.ecr @@ -7,42 +7,6 @@
    <% case account_type when %> - <% when "google" %> - -
    - <% if email %> - - <% else %> - - "> - <% end %> - - <% if password %> - - <% else %> - - "> - <% end %> - - <% if prompt %> - - - <% end %> - - <% if tfa %> - - <% end %> - - <% if captcha %> - - - - "> - <% end %> - - -
    - <% else # "invidious" %>
    From b2b61ab0a9039f256a3f36cd81af316a514b4ba3 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 10 Jun 2023 17:47:33 +0200 Subject: [PATCH 248/598] User: Remove broken Google login (login route) --- src/invidious/routes/login.cr | 274 +--------------------------------- 1 file changed, 2 insertions(+), 272 deletions(-) diff --git a/src/invidious/routes/login.cr b/src/invidious/routes/login.cr index 6454131a..ca1e0d49 100644 --- a/src/invidious/routes/login.cr +++ b/src/invidious/routes/login.cr @@ -24,9 +24,6 @@ module Invidious::Routes::Login captcha_type = env.params.query["captcha"]? captcha_type ||= "image" - tfa = env.params.query["tfa"]? - prompt = nil - templated "user/login" end @@ -47,283 +44,18 @@ module Invidious::Routes::Login account_type ||= "invidious" case account_type - when "google" - tfa_code = env.params.body["tfa"]?.try &.lchop("G-") - traceback = IO::Memory.new - - # See https://github.com/ytdl-org/youtube-dl/blob/2019.04.07/youtube_dl/extractor/youtube.py#L82 - begin - client = nil # Declare variable - {% unless flag?(:disable_quic) %} - client = CONFIG.use_quic ? QUIC::Client.new(LOGIN_URL) : HTTP::Client.new(LOGIN_URL) - {% else %} - client = HTTP::Client.new(LOGIN_URL) - {% end %} - - headers = HTTP::Headers.new - - login_page = client.get("/ServiceLogin") - headers = login_page.cookies.add_request_headers(headers) - - lookup_req = { - email, nil, [] of String, nil, "US", nil, nil, 2, false, true, - {nil, nil, - {2, 1, nil, 1, - "https://accounts.google.com/ServiceLogin?passive=true&continue=https%3A%2F%2Fwww.youtube.com%2Fsignin%3Fnext%3D%252F%26action_handle_signin%3Dtrue%26hl%3Den%26app%3Ddesktop%26feature%3Dsign_in_button&hl=en&service=youtube&uilel=3&requestPath=%2FServiceLogin&Page=PasswordSeparationSignIn", - nil, [] of String, 4}, - 1, - {nil, nil, [] of String}, - nil, nil, nil, true, - }, - email, - }.to_json - - traceback << "Getting lookup..." - - headers["Content-Type"] = "application/x-www-form-urlencoded;charset=utf-8" - headers["Google-Accounts-XSRF"] = "1" - - response = client.post("/_/signin/sl/lookup", headers, login_req(lookup_req)) - lookup_results = JSON.parse(response.body[5..-1]) - - traceback << "done, returned #{response.status_code}.
    " - - user_hash = lookup_results[0][2] - - if token = env.params.body["token"]? - answer = env.params.body["answer"]? - captcha = {token, answer} - else - captcha = nil - end - - challenge_req = { - user_hash, nil, 1, nil, - {1, nil, nil, nil, - {password, captcha, true}, - }, - {nil, nil, - {2, 1, nil, 1, - "https://accounts.google.com/ServiceLogin?passive=true&continue=https%3A%2F%2Fwww.youtube.com%2Fsignin%3Fnext%3D%252F%26action_handle_signin%3Dtrue%26hl%3Den%26app%3Ddesktop%26feature%3Dsign_in_button&hl=en&service=youtube&uilel=3&requestPath=%2FServiceLogin&Page=PasswordSeparationSignIn", - nil, [] of String, 4}, - 1, - {nil, nil, [] of String}, - nil, nil, nil, true, - }, - }.to_json - - traceback << "Getting challenge..." - - response = client.post("/_/signin/sl/challenge", headers, login_req(challenge_req)) - headers = response.cookies.add_request_headers(headers) - challenge_results = JSON.parse(response.body[5..-1]) - - traceback << "done, returned #{response.status_code}.
    " - - headers["Cookie"] = URI.decode_www_form(headers["Cookie"]) - - if challenge_results[0][3]?.try &.== 7 - return error_template(423, "Account has temporarily been disabled") - end - - if token = challenge_results[0][-1]?.try &.[-1]?.try &.as_h?.try &.["5001"]?.try &.[-1].as_a?.try &.[-1].as_s - account_type = "google" - captcha_type = "image" - prompt = nil - tfa = tfa_code - captcha = {tokens: [token], question: ""} - - return templated "user/login" - end - - if challenge_results[0][-1]?.try &.[5] == "INCORRECT_ANSWER_ENTERED" - return error_template(401, "Incorrect password") - end - - prompt_type = challenge_results[0][-1]?.try &.[0].as_a?.try &.[0][2]? - if {"TWO_STEP_VERIFICATION", "LOGIN_CHALLENGE"}.includes? prompt_type - traceback << "Handling prompt #{prompt_type}.
    " - case prompt_type - when "TWO_STEP_VERIFICATION" - prompt_type = 2 - else # "LOGIN_CHALLENGE" - prompt_type = 4 - end - - # Prefer Authenticator app and SMS over unsupported protocols - if !{6, 9, 12, 15}.includes?(challenge_results[0][-1][0][0][8].as_i) && prompt_type == 2 - tfa = challenge_results[0][-1][0].as_a.select { |auth_type| {6, 9, 12, 15}.includes? auth_type[8] }[0] - - traceback << "Selecting challenge #{tfa[8]}..." - select_challenge = {prompt_type, nil, nil, nil, {tfa[8]}}.to_json - - tl = challenge_results[1][2] - - tfa = client.post("/_/signin/selectchallenge?TL=#{tl}", headers, login_req(select_challenge)).body - tfa = tfa[5..-1] - tfa = JSON.parse(tfa)[0][-1] - - traceback << "done.
    " - else - traceback << "Using challenge #{challenge_results[0][-1][0][0][8]}.
    " - tfa = challenge_results[0][-1][0][0] - end - - if tfa[5] == "QUOTA_EXCEEDED" - return error_template(423, "Quota exceeded, try again in a few hours") - end - - if !tfa_code - account_type = "google" - captcha_type = "image" - - case tfa[8] - when 6, 9 - prompt = "Google verification code" - when 12 - prompt = "Login verification, recovery email: #{tfa[-1][tfa[-1].as_h.keys[0]][0]}" - when 15 - prompt = "Login verification, security question: #{tfa[-1][tfa[-1].as_h.keys[0]][0]}" - else - prompt = "Google verification code" - end - - tfa = nil - captcha = nil - return templated "user/login" - end - - tl = challenge_results[1][2] - - request_type = tfa[8] - case request_type - when 6 # Authenticator app - tfa_req = { - user_hash, nil, 2, nil, - {6, nil, nil, nil, nil, - {tfa_code, false}, - }, - }.to_json - when 9 # Voice or text message - tfa_req = { - user_hash, nil, 2, nil, - {9, nil, nil, nil, nil, nil, nil, nil, - {nil, tfa_code, false, 2}, - }, - }.to_json - when 12 # Recovery email - tfa_req = { - user_hash, nil, 4, nil, - {12, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - {tfa_code}, - }, - }.to_json - when 15 # Security question - tfa_req = { - user_hash, nil, 5, nil, - {15, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, - {tfa_code}, - }, - }.to_json - else - return error_template(500, "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.") - end - - traceback << "Submitting challenge..." - - response = client.post("/_/signin/challenge?hl=en&TL=#{tl}", headers, login_req(tfa_req)) - headers = response.cookies.add_request_headers(headers) - challenge_results = JSON.parse(response.body[5..-1]) - - if (challenge_results[0][-1]?.try &.[5] == "INCORRECT_ANSWER_ENTERED") || - (challenge_results[0][-1]?.try &.[5] == "INVALID_INPUT") - return error_template(401, "Invalid TFA code") - end - - traceback << "done.
    " - end - - traceback << "Logging in..." - - location = URI.parse(challenge_results[0][-1][2].to_s) - cookies = HTTP::Cookies.from_client_headers(headers) - - headers.delete("Content-Type") - headers.delete("Google-Accounts-XSRF") - - loop do - if !location || location.path == "/ManageAccount" - break - end - - # Occasionally there will be a second page after login confirming - # the user's phone number ("/b/0/SmsAuthInterstitial"), which we currently don't handle. - - if location.path.starts_with? "/b/0/SmsAuthInterstitial" - traceback << "Unhandled dialog /b/0/SmsAuthInterstitial." - end - - login = client.get(location.request_target, headers) - - headers = login.cookies.add_request_headers(headers) - location = login.headers["Location"]?.try { |u| URI.parse(u) } - end - - cookies = HTTP::Cookies.from_client_headers(headers) - sid = cookies["SID"]?.try &.value - if !sid - raise "Couldn't get SID." - end - - user, sid = get_user(sid, headers) - - # We are now logged in - traceback << "done.
    " - - host = URI.parse(env.request.headers["Host"]).host - - cookies.each do |cookie| - cookie.secure = Invidious::User::Cookies::SECURE - - if cookie.extension - cookie.extension = cookie.extension.not_nil!.gsub(".youtube.com", host) - cookie.extension = cookie.extension.not_nil!.gsub("Secure; ", "") - end - env.response.cookies << cookie - end - - if env.request.cookies["PREFS"]? - user.preferences = env.get("preferences").as(Preferences) - Invidious::Database::Users.update_preferences(user) - - cookie = env.request.cookies["PREFS"] - cookie.expires = Time.utc(1990, 1, 1) - env.response.cookies << cookie - end - - env.redirect referer - rescue ex - traceback.rewind - # error_message = translate(locale, "Login failed. This may be because two-factor authentication is not turned on for your account.") - error_message = %(#{ex.message}
    Traceback:
    #{traceback.gets_to_end}
    ) - return error_template(500, error_message) - end when "invidious" - if !email + if email.nil? || email.empty? return error_template(401, "User ID is a required field") end - if !password + if password.nil? || password.empty? return error_template(401, "Password is a required field") end user = Invidious::Database::Users.select(email: email) if user - if !user.password - return error_template(400, "Please sign in using 'Log in with Google'") - end - if Crypto::Bcrypt::Password.new(user.password.not_nil!).verify(password.byte_slice(0, 55)) sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32)) Invidious::Database::SessionIDs.insert(sid, email) @@ -367,8 +99,6 @@ module Invidious::Routes::Login captcha_type ||= "image" account_type = "invidious" - tfa = false - prompt = "" if captcha_type == "image" captcha = Invidious::User::Captcha.generate_image(HMAC_KEY) From d3b04ac68c7d85dae0e1e15611666d7c055e2c12 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 10 Jun 2023 17:48:10 +0200 Subject: [PATCH 249/598] User: Remove broken Google login (dedicated captcha route) --- src/invidious/routes/login.cr | 7 ------- src/invidious/routing.cr | 1 - 2 files changed, 8 deletions(-) diff --git a/src/invidious/routes/login.cr b/src/invidious/routes/login.cr index ca1e0d49..d0f7ac22 100644 --- a/src/invidious/routes/login.cr +++ b/src/invidious/routes/login.cr @@ -211,11 +211,4 @@ module Invidious::Routes::Login env.redirect referer end - - def self.captcha(env) - headers = HTTP::Headers{":authority" => "accounts.google.com"} - response = YT_POOL.client &.get(env.request.resource, headers) - env.response.headers["Content-Type"] = response.headers["Content-Type"] - response.body - end end diff --git a/src/invidious/routing.cr b/src/invidious/routing.cr index 72ee9194..daaf4d88 100644 --- a/src/invidious/routing.cr +++ b/src/invidious/routing.cr @@ -57,7 +57,6 @@ module Invidious::Routing get "/login", Routes::Login, :login_page post "/login", Routes::Login, :login post "/signout", Routes::Login, :signout - get "/Captcha", Routes::Login, :captcha # User preferences get "/preferences", Routes::PreferencesRoute, :show From 836898754e35957b9bcec5acc055e0993da8e37b Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 10 Jun 2023 18:00:22 +0200 Subject: [PATCH 250/598] User: Remove broken Google login (before_all route) --- src/invidious/routes/before_all.cr | 56 ++++++++---------------------- 1 file changed, 15 insertions(+), 41 deletions(-) diff --git a/src/invidious/routes/before_all.cr b/src/invidious/routes/before_all.cr index 8e2a253f..396840a4 100644 --- a/src/invidious/routes/before_all.cr +++ b/src/invidious/routes/before_all.cr @@ -80,49 +80,23 @@ module Invidious::Routes::BeforeAll raise "Cannot use token as SID" end - # Invidious users only have SID - if !env.request.cookies.has_key? "SSID" - if email = Invidious::Database::SessionIDs.select_email(sid) - user = Invidious::Database::Users.select!(email: email) - csrf_token = generate_response(sid, { - ":authorize_token", - ":playlist_ajax", - ":signout", - ":subscription_ajax", - ":token_ajax", - ":watch_ajax", - }, HMAC_KEY, 1.week) + if email = Database::SessionIDs.select_email(sid) + user = Database::Users.select!(email: email) + csrf_token = generate_response(sid, { + ":authorize_token", + ":playlist_ajax", + ":signout", + ":subscription_ajax", + ":token_ajax", + ":watch_ajax", + }, HMAC_KEY, 1.week) - preferences = user.preferences - env.set "preferences", preferences + preferences = user.preferences + env.set "preferences", preferences - env.set "sid", sid - env.set "csrf_token", csrf_token - env.set "user", user - end - else - headers = HTTP::Headers.new - headers["Cookie"] = env.request.headers["Cookie"] - - begin - user, sid = get_user(sid, headers, false) - csrf_token = generate_response(sid, { - ":authorize_token", - ":playlist_ajax", - ":signout", - ":subscription_ajax", - ":token_ajax", - ":watch_ajax", - }, HMAC_KEY, 1.week) - - preferences = user.preferences - env.set "preferences", preferences - - env.set "sid", sid - env.set "csrf_token", csrf_token - env.set "user", user - rescue ex - end + env.set "sid", sid + env.set "csrf_token", csrf_token + env.set "user", user end end From fcbd5106c3583601d09ddbaa07a12e4b73552200 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 10 Jun 2023 18:00:42 +0200 Subject: [PATCH 251/598] User: Remove broken Google login (password change route) --- src/invidious/routes/account.cr | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/invidious/routes/account.cr b/src/invidious/routes/account.cr index 5aa4452c..9d930841 100644 --- a/src/invidious/routes/account.cr +++ b/src/invidious/routes/account.cr @@ -42,11 +42,6 @@ module Invidious::Routes::Account sid = sid.as(String) token = env.params.body["csrf_token"]? - # We don't store passwords for Google accounts - if !user.password - return error_template(400, "Cannot change password for Google accounts") - end - begin validate_request(token, sid, env.request, HMAC_KEY, locale) rescue ex @@ -54,7 +49,7 @@ module Invidious::Routes::Account end password = env.params.body["password"]? - if !password + if password.nil? || password.empty? return error_template(401, "Password is a required field") end From 9dd4195dd0089216a42214c7b227398906ad7535 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 10 Jun 2023 18:05:34 +0200 Subject: [PATCH 252/598] User: Remove broken Google login (subscribe route) --- src/invidious/routes/subscriptions.cr | 13 ----------- src/invidious/users.cr | 32 --------------------------- 2 files changed, 45 deletions(-) diff --git a/src/invidious/routes/subscriptions.cr b/src/invidious/routes/subscriptions.cr index 0704c05e..7f9ec592 100644 --- a/src/invidious/routes/subscriptions.cr +++ b/src/invidious/routes/subscriptions.cr @@ -43,11 +43,6 @@ module Invidious::Routes::Subscriptions channel_id = env.params.query["c"]? channel_id ||= "" - if !user.password - # Sync subscriptions with YouTube - subscribe_ajax(channel_id, action, env.request.headers) - end - case action when "action_create_subscription_to_channel" if !user.subscriptions.includes? channel_id @@ -82,14 +77,6 @@ module Invidious::Routes::Subscriptions user = user.as(User) sid = sid.as(String) - if !user.password - # Refresh account - headers = HTTP::Headers.new - headers["Cookie"] = env.request.headers["Cookie"] - - user, sid = get_user(sid, headers) - end - action_takeout = env.params.query["action_takeout"]?.try &.to_i? action_takeout ||= 0 action_takeout = action_takeout == 1 diff --git a/src/invidious/users.cr b/src/invidious/users.cr index b763596b..dc36c61e 100644 --- a/src/invidious/users.cr +++ b/src/invidious/users.cr @@ -91,38 +91,6 @@ def create_user(sid, email, password) return user, sid end -def subscribe_ajax(channel_id, action, env_headers) - headers = HTTP::Headers.new - headers["Cookie"] = env_headers["Cookie"] - - html = YT_POOL.client &.get("/subscription_manager?disable_polymer=1", headers) - - cookies = HTTP::Cookies.from_client_headers(headers) - html.cookies.each do |cookie| - if {"VISITOR_INFO1_LIVE", "YSC", "SIDCC"}.includes? cookie.name - if cookies[cookie.name]? - cookies[cookie.name] = cookie - else - cookies << cookie - end - end - end - headers = cookies.add_request_headers(headers) - - if match = html.body.match(/'XSRF_TOKEN': "(?[^"]+)"/) - session_token = match["session_token"] - - headers["content-type"] = "application/x-www-form-urlencoded" - - post_req = { - session_token: session_token, - } - post_url = "/subscription_ajax?#{action}=1&c=#{channel_id}" - - YT_POOL.client &.post(post_url, headers, form: post_req) - end -end - def get_subscription_feed(user, max_results = 40, page = 1) limit = max_results.clamp(0, MAX_ITEMS_PER_PAGE) offset = (page - 1) * limit From 11ab6ffb32a99df287da0c13f08c8433e6ba067b Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 10 Jun 2023 18:07:07 +0200 Subject: [PATCH 253/598] User: Remove broken Google login (notifications route) --- src/invidious/routes/notifications.cr | 44 --------------------------- 1 file changed, 44 deletions(-) diff --git a/src/invidious/routes/notifications.cr b/src/invidious/routes/notifications.cr index 272a3dc7..8922b740 100644 --- a/src/invidious/routes/notifications.cr +++ b/src/invidious/routes/notifications.cr @@ -24,50 +24,6 @@ module Invidious::Routes::Notifications user = user.as(User) - if !user.password - channel_req = {} of String => String - - channel_req["receive_all_updates"] = env.params.query["receive_all_updates"]? || "true" - channel_req["receive_no_updates"] = env.params.query["receive_no_updates"]? || "" - channel_req["receive_post_updates"] = env.params.query["receive_post_updates"]? || "true" - - channel_req.reject! { |k, v| v != "true" && v != "false" } - - headers = HTTP::Headers.new - headers["Cookie"] = env.request.headers["Cookie"] - - html = YT_POOL.client &.get("/subscription_manager?disable_polymer=1", headers) - - cookies = HTTP::Cookies.from_client_headers(headers) - html.cookies.each do |cookie| - if {"VISITOR_INFO1_LIVE", "YSC", "SIDCC"}.includes? cookie.name - if cookies[cookie.name]? - cookies[cookie.name] = cookie - else - cookies << cookie - end - end - end - headers = cookies.add_request_headers(headers) - - if match = html.body.match(/'XSRF_TOKEN': "(?[^"]+)"/) - session_token = match["session_token"] - else - return env.redirect referer - end - - headers["content-type"] = "application/x-www-form-urlencoded" - channel_req["session_token"] = session_token - - subs = XML.parse_html(html.body) - subs.xpath_nodes(%q(//a[@class="subscription-title yt-uix-sessionlink"]/@href)).each do |channel| - channel_id = channel.content.lstrip("/channel/").not_nil! - channel_req["channel_id"] = channel_id - - YT_POOL.client &.post("/subscription_ajax?action_update_subscription_preferences=1", headers, form: channel_req) - end - end - if redirect env.redirect referer else From 39ff94362e951cf69acb3ab56f2c0d378ca1fcc5 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 10 Jun 2023 19:32:06 +0200 Subject: [PATCH 254/598] User: Remove broken Google login (feeds route) --- src/invidious/routes/feeds.cr | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/invidious/routes/feeds.cr b/src/invidious/routes/feeds.cr index fb482e33..fc62c5a3 100644 --- a/src/invidious/routes/feeds.cr +++ b/src/invidious/routes/feeds.cr @@ -83,10 +83,6 @@ module Invidious::Routes::Feeds headers = HTTP::Headers.new headers["Cookie"] = env.request.headers["Cookie"] - if !user.password - user, sid = get_user(sid, headers) - end - max_results = env.params.query["max_results"]?.try &.to_i?.try &.clamp(0, MAX_ITEMS_PER_PAGE) max_results ||= user.preferences.max_results max_results ||= CONFIG.default_user_preferences.max_results From 34441178182cb96e93d679230103005b86e3b35b Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 10 Jun 2023 18:13:45 +0200 Subject: [PATCH 255/598] User: Remove broken Google login (various constants) --- src/invidious.cr | 1 - src/invidious/yt_backend/connection_pool.cr | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index 27c4775e..636e28a6 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -63,7 +63,6 @@ HMAC_KEY = CONFIG.hmac_key || Random::Secure.hex(32) PG_DB = DB.open CONFIG.database_url ARCHIVE_URL = URI.parse("https://archive.org") -LOGIN_URL = URI.parse("https://accounts.google.com") PUBSUB_URL = URI.parse("https://pubsubhubbub.appspot.com") REDDIT_URL = URI.parse("https://www.reddit.com") YT_URL = URI.parse("https://www.youtube.com") diff --git a/src/invidious/yt_backend/connection_pool.cr b/src/invidious/yt_backend/connection_pool.cr index b4c1878c..658731cf 100644 --- a/src/invidious/yt_backend/connection_pool.cr +++ b/src/invidious/yt_backend/connection_pool.cr @@ -14,8 +14,9 @@ def add_yt_headers(request) request.headers["Accept-Charset"] ||= "ISO-8859-1,utf-8;q=0.7,*;q=0.7" request.headers["Accept"] ||= "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" request.headers["Accept-Language"] ||= "en-us,en;q=0.5" + # Preserve original cookies and add new YT consent cookie for EU servers - request.headers["Cookie"] = "#{request.headers["cookie"]?}; CONSENT=YES+" + request.headers["Cookie"] = "#{request.headers["cookie"]?}; CONSENT=PENDING+#{Random.rand(100..999)}" if !CONFIG.cookies.empty? request.headers["Cookie"] = "#{(CONFIG.cookies.map { |c| "#{c.name}=#{c.value}" }).join("; ")}; #{request.headers["cookie"]?}" end From 69f23d95b8ab719ca4f19649ce105fa29786913c Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 10 Jun 2023 18:04:16 +0200 Subject: [PATCH 256/598] User: Remove broken Google login (various functions) --- src/invidious/helpers/helpers.cr | 25 ------------ src/invidious/users.cr | 69 -------------------------------- 2 files changed, 94 deletions(-) diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr index c3b53339..23ff0da9 100644 --- a/src/invidious/helpers/helpers.cr +++ b/src/invidious/helpers/helpers.cr @@ -22,31 +22,6 @@ struct Annotation property annotations : String end -def login_req(f_req) - data = { - # Unfortunately there's not much information available on `bgRequest`; part of Google's BotGuard - # Generally this is much longer (>1250 characters), see also - # https://github.com/ytdl-org/youtube-dl/commit/baf67a604d912722b0fe03a40e9dc5349a2208cb . - # For now this can be empty. - "bgRequest" => %|["identifier",""]|, - "pstMsg" => "1", - "checkConnection" => "youtube", - "checkedDomains" => "youtube", - "hl" => "en", - "deviceinfo" => %|[null,null,null,[],null,"US",null,null,[],"GlifWebSignIn",null,[null,null,[]]]|, - "f.req" => f_req, - "flowName" => "GlifWebSignIn", - "flowEntry" => "ServiceLogin", - # "cookiesDisabled" => "false", - # "gmscoreversion" => "undefined", - # "continue" => "https://accounts.google.com/ManageAccount", - # "azt" => "", - # "bgHash" => "", - } - - return HTTP::Params.encode(data) -end - def html_to_content(description_html : String) description = description_html.gsub(/(
    )|()/, { "
    ": "\n", diff --git a/src/invidious/users.cr b/src/invidious/users.cr index dc36c61e..65566d20 100644 --- a/src/invidious/users.cr +++ b/src/invidious/users.cr @@ -3,75 +3,6 @@ require "crypto/bcrypt/password" # Materialized views may not be defined using bound parameters (`$1` as used elsewhere) MATERIALIZED_VIEW_SQL = ->(email : String) { "SELECT cv.* FROM channel_videos cv WHERE EXISTS (SELECT subscriptions FROM users u WHERE cv.ucid = ANY (u.subscriptions) AND u.email = E'#{email.gsub({'\'' => "\\'", '\\' => "\\\\"})}') ORDER BY published DESC" } -def get_user(sid, headers, refresh = true) - if email = Invidious::Database::SessionIDs.select_email(sid) - user = Invidious::Database::Users.select!(email: email) - - if refresh && Time.utc - user.updated > 1.minute - user, sid = fetch_user(sid, headers) - - Invidious::Database::Users.insert(user, update_on_conflict: true) - Invidious::Database::SessionIDs.insert(sid, user.email, handle_conflicts: true) - - begin - view_name = "subscriptions_#{sha256(user.email)}" - PG_DB.exec("CREATE MATERIALIZED VIEW #{view_name} AS #{MATERIALIZED_VIEW_SQL.call(user.email)}") - rescue ex - end - end - else - user, sid = fetch_user(sid, headers) - - Invidious::Database::Users.insert(user, update_on_conflict: true) - Invidious::Database::SessionIDs.insert(sid, user.email, handle_conflicts: true) - - begin - view_name = "subscriptions_#{sha256(user.email)}" - PG_DB.exec("CREATE MATERIALIZED VIEW #{view_name} AS #{MATERIALIZED_VIEW_SQL.call(user.email)}") - rescue ex - end - end - - return user, sid -end - -def fetch_user(sid, headers) - feed = YT_POOL.client &.get("/subscription_manager?disable_polymer=1", headers) - feed = XML.parse_html(feed.body) - - channels = feed.xpath_nodes(%q(//ul[@id="guide-channels"]/li/a)).compact_map do |channel| - if {"Popular on YouTube", "Music", "Sports", "Gaming"}.includes? channel["title"] - nil - else - channel["href"].lstrip("/channel/") - end - end - - channels = get_batch_channels(channels) - - email = feed.xpath_node(%q(//a[@class="yt-masthead-picker-header yt-masthead-picker-active-account"])) - if email - email = email.content.strip - else - email = "" - end - - token = Base64.urlsafe_encode(Random::Secure.random_bytes(32)) - - user = Invidious::User.new({ - updated: Time.utc, - notifications: [] of String, - subscriptions: channels, - email: email, - preferences: Preferences.new(CONFIG.default_user_preferences.to_tuple), - password: nil, - token: token, - watched: [] of String, - feed_needs_update: true, - }) - return user, sid -end - def create_user(sid, email, password) password = Crypto::Bcrypt::Password.create(password, cost: 10) token = Base64.urlsafe_encode(Random::Secure.random_bytes(32)) From b06c87ff8d1b799de8926d8b965cc1223b52a3de Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 10 Jun 2023 17:59:50 +0200 Subject: [PATCH 257/598] User: Remove broken Google login (various comments) --- config/config.example.yml | 3 +-- src/invidious/routes/api/v1/authenticated.cr | 4 ---- src/invidious/routes/playlists.cr | 4 ---- src/invidious/views/privacy.ecr | 3 +-- 4 files changed, 2 insertions(+), 12 deletions(-) diff --git a/config/config.example.yml b/config/config.example.yml index 7ea80017..c591eb6a 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -255,8 +255,7 @@ https_only: false #registration_enabled: true ## -## Allow/Forbid users to log-in. This setting affects the ability -## to connect with BOTH Google and Invidious (local) accounts. +## Allow/Forbid users to log-in. ## ## Accepted values: true, false ## Default: true diff --git a/src/invidious/routes/api/v1/authenticated.cr b/src/invidious/routes/api/v1/authenticated.cr index ce2ee812..a35d2f2b 100644 --- a/src/invidious/routes/api/v1/authenticated.cr +++ b/src/invidious/routes/api/v1/authenticated.cr @@ -178,10 +178,6 @@ module Invidious::Routes::API::V1::Authenticated Invidious::Database::Users.subscribe_channel(user, ucid) end - # For Google accounts, access tokens don't have enough information to - # make a request on the user's behalf, which is why we don't sync with - # YouTube. - env.response.status_code = 204 end diff --git a/src/invidious/routes/playlists.cr b/src/invidious/routes/playlists.cr index 8675fa45..1dd3f32e 100644 --- a/src/invidious/routes/playlists.cr +++ b/src/invidious/routes/playlists.cr @@ -320,10 +320,6 @@ module Invidious::Routes::Playlists end end - if !user.password - # TODO: Playlist stub, sync with YouTube for Google accounts - # playlist_ajax(playlist_id, action, env.request.headers) - end email = user.email case action diff --git a/src/invidious/views/privacy.ecr b/src/invidious/views/privacy.ecr index 643f880b..bc5ff40b 100644 --- a/src/invidious/views/privacy.ecr +++ b/src/invidious/views/privacy.ecr @@ -16,12 +16,11 @@
  • a list of channel UCIDs the user is subscribed to
  • a user ID (for persistent storage of subscriptions and preferences)
  • a json object containing user preferences
  • -
  • a hashed password if applicable (not present on google accounts)
  • +
  • a hashed password
  • a randomly generated token for providing an RSS feed of a user's subscriptions
  • a list of video IDs identifying watched videos
  • Users can clear their watch history using the clear watch history page.

    -

    If a user is logged in with a Google account, no password will ever be stored. This website uses the session token provided by Google to identify a user, but does not store the information required to make requests on a user's behalf without their knowledge or consent.

    Data you passively provide

    When you request any resource from this website (for example: a page, a font, an image, or an API endpoint) information about the request may be logged.

    From 8e4833d21a08b9a25cd15738a399c64bc5575fa6 Mon Sep 17 00:00:00 2001 From: Emilien Devos <4016501+unixfox@users.noreply.github.com> Date: Sun, 11 Jun 2023 16:37:27 +0200 Subject: [PATCH 258/598] temp explanation about video not available issue --- src/invidious/videos/parser.cr | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr index 2e8eecc3..9cc0ffdc 100644 --- a/src/invidious/videos/parser.cr +++ b/src/invidious/videos/parser.cr @@ -78,7 +78,11 @@ def extract_video_info(video_id : String, proxy_region : String? = nil) elsif video_id != player_response.dig("videoDetails", "videoId") # YouTube may return a different video player response than expected. # See: https://github.com/TeamNewPipe/NewPipe/issues/8713 - raise VideoNotAvailableException.new("The video returned by YouTube isn't the requested one. (WEB client)") + # Line to be reverted if one day we solve the video not available issue. + return { + "version" => JSON::Any.new(Video::SCHEMA_VERSION.to_i64), + "reason" => JSON::Any.new("Can't load the video on this Invidious instance. YouTube is currently trying to block Invidious instances. Click here for more info about the issue."), + } else reason = nil end From 7a569d81ca0877ac081d7aa89a2acaf7f6d08940 Mon Sep 17 00:00:00 2001 From: lamemakes Date: Mon, 12 Jun 2023 09:40:26 -0400 Subject: [PATCH 259/598] Updated comment link returns --- src/invidious/helpers/utils.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index 48bf769f..a006d602 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -440,7 +440,7 @@ def parse_link_endpoint(endpoint : JSON::Any, text : String, video_id : String) # - https://github.com/iv-org/invidious/issues/3062 text = %(#{text}) else - text = %(#{reduce_uri(url)}) + text = %(#{reduce_uri(text)}) end end return text From 495ccdc221205572dd4d34d94b0d9e3d27a79e7a Mon Sep 17 00:00:00 2001 From: Ikko Eltociear Ashimine Date: Tue, 13 Jun 2023 19:16:07 +0900 Subject: [PATCH 260/598] Fix typo in jobs.cr follwing -> following --- src/invidious/jobs.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/jobs.cr b/src/invidious/jobs.cr index 524a3624..b6b673f7 100644 --- a/src/invidious/jobs.cr +++ b/src/invidious/jobs.cr @@ -2,7 +2,7 @@ module Invidious::Jobs JOBS = [] of BaseJob # Automatically generate a structure that wraps the various - # jobs' configs, so that the follwing YAML config can be used: + # jobs' configs, so that the following YAML config can be used: # # jobs: # job_name: From 16b8b6034fed818bc05fbdea80b50bb04e1055f7 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Wed, 21 Jun 2023 21:41:53 +0200 Subject: [PATCH 261/598] Channels: Use new ctoken value for "sort by oldest" --- src/invidious/channels/videos.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/channels/videos.cr b/src/invidious/channels/videos.cr index 12ed4a7d..beb86e08 100644 --- a/src/invidious/channels/videos.cr +++ b/src/invidious/channels/videos.cr @@ -20,7 +20,7 @@ def produce_channel_videos_continuation(ucid, page = 1, auto_generated = nil, so case sort_by when "newest" then 1_i64 when "popular" then 2_i64 - when "oldest" then 3_i64 # Broken as of 10/2022 :c + when "oldest" then 4_i64 else 1_i64 # Fallback to "newest" end From c46d867f177fd824778510e33d473f470727896c Mon Sep 17 00:00:00 2001 From: Rex_sa Date: Sun, 11 Jun 2023 21:04:15 +0000 Subject: [PATCH 262/598] Update Arabic translation --- locales/ar.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/locales/ar.json b/locales/ar.json index 2e275e77..c137d1a3 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -48,8 +48,8 @@ "preferences_category_player": "إعدادات المُشغِّل", "preferences_video_loop_label": "كرر المقطع المرئيّ دائما: ", "preferences_autoplay_label": "تشغيل تلقائي: ", - "preferences_continue_label": "شغل المقطع التالي تلقائيًا: ", - "preferences_continue_autoplay_label": "شغل المقطع التالي تلقائيًا: ", + "preferences_continue_label": "تشغيل المقطع التالي تلقائيًا: ", + "preferences_continue_autoplay_label": "شغل المقطع التالي تلقائيًا: . ", "preferences_listen_label": "تشغيل النسخة السمعية تلقائيًا: ", "preferences_local_label": "بروكسي المقاطع المرئيّة؟ ", "preferences_speed_label": "السرعة الافتراضية: ", @@ -155,7 +155,7 @@ "View more comments on Reddit": "عرض المزيد من التعليقات على\\من موقع ريديت", "View `x` comments": { "([^.,0-9]|^)1([^.,0-9]|$)": "عرض `x` تعليقات", - "": "عرض `x` تعليقات" + "": "عرض `x` تعليقات." }, "View Reddit comments": "عرض تعليقات ريديت", "Hide replies": "إخفاء الردود", From 4645c587122acb9fea326c85ad3ca805def17965 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Tue, 13 Jun 2023 11:48:56 +0000 Subject: [PATCH 263/598] =?UTF-8?q?Update=20Norwegian=20Bokm=C3=A5l=20tran?= =?UTF-8?q?slation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/nb-NO.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/locales/nb-NO.json b/locales/nb-NO.json index 05cc7328..1e0e9e77 100644 --- a/locales/nb-NO.json +++ b/locales/nb-NO.json @@ -464,5 +464,17 @@ "search_filters_apply_button": "Bruk valgte filtre", "search_filters_date_option_none": "Siden begynnelsen", "search_filters_features_option_vr180": "VR180", - "error_video_not_in_playlist": "Forespurt video finnes ikke i denne spillelisten. Trykk her for spillelistens hjemmeside." + "error_video_not_in_playlist": "Forespurt video finnes ikke i denne spillelisten. Trykk her for spillelistens hjemmeside.", + "Standard YouTube license": "Standard YouTube-lisens", + "Song: ": "Sang: ", + "channel_tab_streams_label": "Direktesendinger", + "channel_tab_shorts_label": "Kortvideoer", + "channel_tab_playlists_label": "Spillelister", + "Music in this video": "Musikk i denne videoen", + "channel_tab_channels_label": "Kanaler", + "Artist: ": "Artist: ", + "Album: ": "Album: ", + "Download is disabled": "Nedlasting er avskrudd", + "Channel Sponsor": "Kanalsponsor", + "Import YouTube playlist (.csv)": "Importer YouTube-spilleliste (.csv)" } From b13b7646b73b2617744e67371bc2276cceb3ee20 Mon Sep 17 00:00:00 2001 From: Schuetzer Date: Tue, 13 Jun 2023 14:24:08 +0000 Subject: [PATCH 264/598] Update Vietnamese translation --- locales/vi.json | 79 ++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 62 insertions(+), 17 deletions(-) diff --git a/locales/vi.json b/locales/vi.json index 42076745..d79c684c 100644 --- a/locales/vi.json +++ b/locales/vi.json @@ -1,10 +1,10 @@ { "generic_videos_count_0": "{{count}} video", - "generic_subscribers_count_0": "{{count}} subscribers", + "generic_subscribers_count_0": "{{count}} người theo dõi", "LIVE": "TRỰC TIẾP", "Shared `x` ago": "Đã chia sẻ` x` trước", - "Unsubscribe": "Hủy đăng ký", - "Subscribe": "Đăng ký", + "Unsubscribe": "Hủy theo dõi", + "Subscribe": "Theo dõi", "View channel on YouTube": "Xem kênh trên YouTube", "View playlist on YouTube": "Xem danh sách phát trên YouTube", "newest": "mới nhất", @@ -22,15 +22,15 @@ "No": "Không", "Import and Export Data": "Nhập và xuất dữ liệu", "Import": "Nhập", - "Import Invidious data": "Nhập dữ liệu sống động", - "Import YouTube subscriptions": "Nhập đăng ký YouTube", + "Import Invidious data": "Nhập dữ liệu Invidious JSON", + "Import YouTube subscriptions": "Nhập dữ liệu thuê bao YouTube/OPML", "Import FreeTube subscriptions (.db)": "Nhập đăng ký FreeTube (.db)", "Import NewPipe subscriptions (.json)": "Nhập đăng ký NewPipe (.json)", "Import NewPipe data (.zip)": "Nhập dữ liệu NewPipe (.zip)", "Export": "Xuất", "Export subscriptions as OPML": "Xuất đăng ký dưới dạng OPML", "Export subscriptions as OPML (for NewPipe & FreeTube)": "Xuất đăng ký dưới dạng OPML (cho NewPipe & FreeTube)", - "Export data as JSON": "Xuất dữ liệu dưới dạng JSON", + "Export data as JSON": "Xuất dữ liệu Invidious dưới dạng JSON", "Delete account?": "Xóa tài khoản?", "History": "Lịch sử", "An alternative front-end to YouTube": "Giao diện người dùng thay thế cho YouTube", @@ -47,34 +47,34 @@ "Register": "Đăng ký", "E-mail": "E-mail", "Preferences": "Sở thích", - "preferences_category_player": "Tùy chọn người chơi", + "preferences_category_player": "Tùy chọn trình phát video", "preferences_video_loop_label": "Luôn lặp lại: ", "preferences_autoplay_label": "Tự chạy: ", - "preferences_continue_label": "Phát tiếp theo theo mặc định: ", + "preferences_continue_label": "Phát kế tiếp theo mặc định: ", "preferences_continue_autoplay_label": "Tự động phát video tiếp theo: ", "preferences_listen_label": "Nghe theo mặc định: ", "preferences_local_label": "Video proxy: ", "preferences_speed_label": "Tốc độ mặc định: ", "preferences_quality_label": "Chất lượng video ưa thích: ", - "preferences_volume_label": "Khối lượng trình phát: ", + "preferences_volume_label": "Âm lượng trình phát video: ", "preferences_comments_label": "Nhận xét mặc định: ", "youtube": "YouTube", - "reddit": "reddit", + "reddit": "Reddit", "preferences_captions_label": "Phụ đề mặc định: ", "Fallback captions: ": "Phụ đề dự phòng: ", "preferences_related_videos_label": "Hiển thị các video có liên quan: ", "preferences_annotations_label": "Hiển thị chú thích theo mặc định: ", "preferences_extend_desc_label": "Tự động mở rộng mô tả video: ", - "preferences_vr_mode_label": "Video 360 độ tương tác: ", + "preferences_vr_mode_label": "Video 360 độ tương tác (yêu cầu WebGL): ", "preferences_category_visual": "Tùy chọn hình ảnh", - "preferences_player_style_label": "Phong cách người chơi: ", + "preferences_player_style_label": "Phong cách trình phát: ", "Dark mode: ": "Chế độ tối: ", "preferences_dark_mode_label": "Chủ đề: ", "dark": "tối", "light": "ánh sáng", "preferences_thin_mode_label": "Chế độ mỏng: ", "preferences_category_misc": "Tùy chọn khác", - "preferences_automatic_instance_redirect_label": "Chuyển hướng phiên bản tự động (dự phòng thành redirect.invidious.io): ", + "preferences_automatic_instance_redirect_label": "Tự động chuyển hướng phiên bản (dự phòng về redirect.invidious.io): ", "preferences_category_subscription": "Tùy chọn đăng ký", "preferences_annotations_subscribed_label": "Hiển thị chú thích theo mặc định cho các kênh đã đăng ký: ", "Redirect homepage to feed: ": "Chuyển hướng trang chủ đến nguồn cấp dữ liệu: ", @@ -114,14 +114,14 @@ "Subscription manager": "Người quản lý đăng ký", "Token manager": "Trình quản lý mã thông báo", "Token": "Mã thông báo", - "search": "Tìm kiếm", + "search": "tìm kiếm", "Log out": "Đăng xuất", "Source available here.": "Nguồn có sẵn ở đây.", "View JavaScript license information.": "Xem thông tin giấy phép JavaScript.", "View privacy policy.": "Xem chính sách bảo mật.", "Trending": "Xu hướng", "Public": "Công cộng", - "Unlisted": "Riêng tư", + "Unlisted": "Không hiển thị", "Private": "Riêng tư", "View all playlists": "Xem tất cả danh sách phát", "Updated `x` ago": "Đã cập nhật` x` trước", @@ -337,6 +337,51 @@ "generic_playlists_count": "{{count}} danh sách phát", "generic_views_count": "{{count}} lượt xem", "View `x` comments": { - "": "Xem `x` bình luận" - } + "": "Xem `x` bình luận", + "([^.,0-9]|^)1([^.,0-9]|$)": "Hiển thị `x`bình luận" + }, + "Song: ": "Ca khúc: ", + "Premieres in `x`": "Trình chiếu lần đầu vào `x`", + "preferences_quality_dash_option_worst": "Thấp nhất", + "preferences_watch_history_label": "Bật lịch sử video đã xem ", + "preferences_quality_option_hd720": "HD720", + "unsubscribe": "hủy đăng kí", + "revoke": "gỡ bỏ", + "preferences_quality_dash_label": "Chất lượng video DASH ưa thích ", + "preferences_quality_dash_option_auto": "Tự động", + "Subscriptions": "Thuê bao", + "View YouTube comments": "Hiển thị bình luận trên YouTube", + "View more comments on Reddit": "Hiển thị thêm bình luận từ Reddit", + "Music in this video": "Nhạc trong video này", + "Artist: ": "Nghệ sĩ: ", + "Premieres `x`": "Phát lần đầu `x`", + "preferences_region_label": "Nội dung theo quốc gia ", + "search_message_change_filters_or_query": "Thử mở rộng nội dung tìm kiếm hoặc thay đổi bộ lọc.", + "preferences_quality_option_small": "Nhỏ", + "preferences_quality_dash_option_144p": "144p", + "invidious": "Invidious", + "preferences_quality_dash_option_240p": "240p", + "Import/export": "Xuất/nhập dữ liệu", + "preferences_quality_dash_option_4320p": "4320p", + "preferences_quality_option_dash": "DASH (tự tối ưu chất lượng)", + "generic_subscriptions_count_0": "{{count}} thuê bao", + "preferences_quality_dash_option_1440p": "1440p", + "preferences_quality_dash_option_480p": "480p", + "preferences_quality_dash_option_2160p": "2160p", + "search_message_no_results": "Tìm kiếm không có kết quả.", + "preferences_quality_dash_option_1080p": "1080p", + "preferences_quality_dash_option_720p": "720p", + "preferences_quality_option_medium": "Trung bình", + "Load more": "Hiển thị thêm", + "comments_points_count_0": "{{count}} điểm", + "Import YouTube playlist (.csv)": "Nhập danh sách phát YouTube (.csv)", + "preferences_quality_dash_option_best": "Tốt nhất", + "preferences_quality_dash_option_360p": "360p", + "subscriptions_unseen_notifs_count_0": "{{count}} thông báo chưa đọc", + "Released under the AGPLv3 on Github.": "Phát hành dưới giấy phép AGPLv3 trên GitHub.", + "search_message_use_another_instance": " Bạn cũng có thể tìm kiếm ở một phiên bản khác.", + "Standard YouTube license": "Giấy phép YouTube thông thường", + "Album: ": "Album: ", + "preferences_save_player_pos_label": "Lưu vị trí xem cuối cùng ", + "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Xin chào! Có vẻ như bạn đã tắt JavaScript. Bấm vào đây để xem bình luận, lưu ý rằng thời gian tải có thể lâu hơn." } From efce7c338e5d558ed8c99eea7aa90c46788c4ac7 Mon Sep 17 00:00:00 2001 From: 04f7rx0n6 <04f7rx0n6@proton.me> Date: Thu, 15 Jun 2023 08:08:21 +0000 Subject: [PATCH 265/598] Update Russian translation --- locales/ru.json | 50 ++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/locales/ru.json b/locales/ru.json index 7f79a90c..a93207ad 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -4,7 +4,7 @@ "Unsubscribe": "Отписаться", "Subscribe": "Подписаться", "View channel on YouTube": "Смотреть канал на YouTube", - "View playlist on YouTube": "Просмотреть подборку на ютубе", + "View playlist on YouTube": "Посмотреть плейлист на YouTube", "newest": "сначала новые", "oldest": "сначала старые", "popular": "популярные", @@ -126,14 +126,14 @@ "Public": "Публичный", "Unlisted": "Нет в списке", "Private": "Приватный", - "View all playlists": "Просмотреть все подборки", + "View all playlists": "Посмотреть все плейлисты", "Updated `x` ago": "Обновлено `x` назад", - "Delete playlist `x`?": "Удалить подборку `x`?", - "Delete playlist": "Удалить подборку", - "Create playlist": "Создать подборку", + "Delete playlist `x`?": "Удалить плейлист `x`?", + "Delete playlist": "Удалить плейлист", + "Create playlist": "Создать плейлист", "Title": "Заголовок", - "Playlist privacy": "Видимость подборки", - "Editing playlist `x`": "Изменение подборки `x`", + "Playlist privacy": "Видимость плейлиста", + "Editing playlist `x`": "Редактирование плейлиста `x`", "Show more": "Развернуть", "Show less": "Свернуть", "Watch on YouTube": "Смотреть на YouTube", @@ -179,9 +179,9 @@ "`x` ago": "`x` назад", "Load more": "Загрузить ещё", "Could not create mix.": "Не удалось создать микс.", - "Empty playlist": "Подборка пуста", - "Not a playlist.": "Это не подборка.", - "Playlist does not exist.": "Подборка не существует.", + "Empty playlist": "Плейлист пуст", + "Not a playlist.": "Это не плейлист.", + "Playlist does not exist.": "Плейлист не существует.", "Could not pull trending pages.": "Не удаётся загрузить страницы «в тренде».", "Hidden field \"challenge\" is a required field": "Необходимо заполнить скрытое поле «challenge»", "Hidden field \"token\" is a required field": "Необходимо заполнить скрытое поле «токен»", @@ -302,7 +302,7 @@ "About": "О сайте", "Rating: ": "Рейтинг: ", "preferences_locale_label": "Язык: ", - "View as playlist": "Смотреть как подборку", + "View as playlist": "Смотреть как плейлист", "Default": "По умолчанию", "Music": "Музыка", "Gaming": "Игры", @@ -318,16 +318,16 @@ "Audio mode": "Аудио режим", "Video mode": "Видео режим", "channel_tab_videos_label": "Видео", - "Playlists": "Подборки", + "Playlists": "Плейлисты", "channel_tab_community_label": "Сообщество", - "search_filters_sort_option_relevance": "по актуальности", - "search_filters_sort_option_rating": "по рейтингу", - "search_filters_sort_option_date": "по дате загрузки", - "search_filters_sort_option_views": "по просмотрам", + "search_filters_sort_option_relevance": "актуальности", + "search_filters_sort_option_rating": "рейтингу", + "search_filters_sort_option_date": "дате загрузки", + "search_filters_sort_option_views": "просмотрам", "search_filters_type_label": "Тип", "search_filters_duration_label": "Длительность", "search_filters_features_label": "Дополнительно", - "search_filters_sort_label": "Сортировать", + "search_filters_sort_label": "Сортировать по", "search_filters_date_option_hour": "Последний час", "search_filters_date_option_today": "Сегодня", "search_filters_date_option_week": "Эта неделя", @@ -335,7 +335,7 @@ "search_filters_date_option_year": "Этот год", "search_filters_type_option_video": "Видео", "search_filters_type_option_channel": "Канал", - "search_filters_type_option_playlist": "Подборка", + "search_filters_type_option_playlist": "Плейлист", "search_filters_type_option_movie": "Фильм", "search_filters_type_option_show": "Сериал", "search_filters_features_option_hd": "HD", @@ -377,7 +377,7 @@ "videoinfo_youTube_embed_link": "Версия для встраивания", "videoinfo_invidious_embed_link": "Ссылка для встраивания", "download_subtitles": "Субтитры - `x` (.vtt)", - "user_created_playlists": "`x` созданных подборок", + "user_created_playlists": "`x` созданных плейлистов", "crash_page_you_found_a_bug": "Похоже, вы нашли ошибку в Invidious!", "crash_page_before_reporting": "Прежде чем сообщать об ошибке, убедитесь, что вы:", "crash_page_refresh": "пробовали перезагрузить страницу", @@ -385,9 +385,9 @@ "generic_videos_count_0": "{{count}} видео", "generic_videos_count_1": "{{count}} видео", "generic_videos_count_2": "{{count}} видео", - "generic_playlists_count_0": "{{count}} подборка", - "generic_playlists_count_1": "{{count}} подборки", - "generic_playlists_count_2": "{{count}} подборок", + "generic_playlists_count_0": "{{count}} плейлист", + "generic_playlists_count_1": "{{count}} плейлиста", + "generic_playlists_count_2": "{{count}} плейлистов", "tokens_count_0": "{{count}} токен", "tokens_count_1": "{{count}} токена", "tokens_count_2": "{{count}} токенов", @@ -446,7 +446,7 @@ "footer_source_code": "Исходный код", "footer_original_source_code": "Оригинальный исходный код", "footer_modfied_source_code": "Изменённый исходный код", - "user_saved_playlists": "`x` сохранённых подборок", + "user_saved_playlists": "`x` сохранённых плейлистов", "crash_page_search_issue": "поискали похожую проблему на GitHub", "comments_points_count_0": "{{count}} плюс", "comments_points_count_1": "{{count}} плюса", @@ -480,8 +480,8 @@ "search_filters_duration_option_medium": "Средние (4 - 20 минут)", "search_filters_apply_button": "Применить фильтры", "Popular enabled: ": "Популярное включено: ", - "error_video_not_in_playlist": "Запрошенного видео нет в этой подборке. Нажмите тут, чтобы вернуться к странице подборки.", - "channel_tab_playlists_label": "Подборки", + "error_video_not_in_playlist": "Запрошенного видео нет в этом плейлисте. Нажмите тут, чтобы вернуться к странице плейлиста.", + "channel_tab_playlists_label": "Плейлисты", "channel_tab_channels_label": "Каналы", "channel_tab_streams_label": "Стримы", "channel_tab_shorts_label": "Shorts", From 1255f5989b88a0c08fbd812b27ee86d1e6f4e777 Mon Sep 17 00:00:00 2001 From: SC Date: Thu, 15 Jun 2023 17:55:32 +0000 Subject: [PATCH 266/598] Update Portuguese translation --- locales/pt.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/pt.json b/locales/pt.json index c817460a..dfa411c3 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -475,5 +475,6 @@ "Song: ": "Canção: ", "Channel Sponsor": "Patrocinador do canal", "Standard YouTube license": "Licença padrão do YouTube", - "Download is disabled": "A descarga está desativada" + "Download is disabled": "A descarga está desativada", + "Import YouTube playlist (.csv)": "Importar lista de reprodução do YouTube (.csv)" } From 59cc637c655580305bd0eedb80aa14bb6087531d Mon Sep 17 00:00:00 2001 From: Damjan Gerl Date: Fri, 16 Jun 2023 11:34:53 +0000 Subject: [PATCH 267/598] Update Slovenian translation --- locales/sl.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/sl.json b/locales/sl.json index 592ba78f..45f63c6b 100644 --- a/locales/sl.json +++ b/locales/sl.json @@ -507,5 +507,6 @@ "Song: ": "Pesem: ", "Standard YouTube license": "Standardna licenca YouTube", "Channel Sponsor": "Sponzor kanala", - "Download is disabled": "Prenos je onemogočen" + "Download is disabled": "Prenos je onemogočen", + "Import YouTube playlist (.csv)": "Uvoz seznama predvajanja YouTube (.csv)" } From 0a2d799f6a62a8e2586180b2bb745f6b31b8d1f8 Mon Sep 17 00:00:00 2001 From: Sergi Font Date: Wed, 21 Jun 2023 09:04:03 +0000 Subject: [PATCH 268/598] Update Catalan translation --- locales/ca.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/ca.json b/locales/ca.json index 6a320b02..4392c2a9 100644 --- a/locales/ca.json +++ b/locales/ca.json @@ -475,5 +475,6 @@ "Engagement: ": "Atracció: ", "Redirect homepage to feed: ": "Redirigeix la pàgina d'inici al feed: ", "Standard YouTube license": "Llicència estàndard de YouTube", - "Download is disabled": "Les baixades s'han inhabilitat" + "Download is disabled": "Les baixades s'han inhabilitat", + "Import YouTube playlist (.csv)": "Importar llista de reproducció de YouTube (.csv)" } From b4beae7418def2c648832104eb22e37d6915d8a7 Mon Sep 17 00:00:00 2001 From: maboroshin Date: Fri, 23 Jun 2023 15:00:41 +0000 Subject: [PATCH 269/598] Update Japanese translation --- locales/ja.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/ja.json b/locales/ja.json index 80e28460..8adcbf6a 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -53,7 +53,7 @@ "preferences_category_player": "プレイヤーの設定", "preferences_video_loop_label": "常にループ: ", "preferences_autoplay_label": "自動再生: ", - "preferences_continue_label": "次の動画を再生をオン: ", + "preferences_continue_label": "次の動画を自動再生: ", "preferences_continue_autoplay_label": "次の動画を自動再生: ", "preferences_listen_label": "音声モードを使用: ", "preferences_local_label": "動画視聴にプロキシを経由: ", From 8d6570d809546bea425116201d7511e0c33650ca Mon Sep 17 00:00:00 2001 From: LopeKinz Date: Mon, 26 Jun 2023 09:18:40 +0000 Subject: [PATCH 270/598] 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 5703a0d7..66f2ae6f 100644 --- a/locales/de.json +++ b/locales/de.json @@ -475,5 +475,6 @@ "Channel Sponsor": "Kanalsponsor", "Standard YouTube license": "Standard YouTube-Lizenz", "Song: ": "Musik: ", - "Download is disabled": "Herunterladen ist deaktiviert" + "Download is disabled": "Herunterladen ist deaktiviert", + "Import YouTube playlist (.csv)": "YouTube Playlist Importieren (.csv)" } From d9ae22e97937c4454107e512aa2756822bd71c0a Mon Sep 17 00:00:00 2001 From: Robin Pringle Date: Tue, 27 Jun 2023 06:33:44 +0000 Subject: [PATCH 271/598] Update Afrikaans translation --- locales/af.json | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/locales/af.json b/locales/af.json index 0967ef42..4db98a8b 100644 --- a/locales/af.json +++ b/locales/af.json @@ -1 +1,14 @@ -{} +{ + "generic_views_count": "{{count}} kyk", + "generic_views_count_plural": "{{count}} kyke", + "generic_videos_count": "{{count}} video", + "generic_videos_count_plural": "{{count}} videos", + "generic_playlists_count": "{{count}} snitlys", + "generic_playlists_count_plural": "{{count}} snitlyste", + "generic_subscriptions_count": "{{count}} intekening", + "generic_subscriptions_count_plural": "{{count}} intekeninge", + "LIVE": "LEWENDIG", + "generic_subscribers_count": "{{count}} intekenaar", + "generic_subscribers_count_plural": "{{count}} intekenare", + "Shared `x` ago": "`x` gelede gedeel" +} From 61a18e9894cfca02435b5804c134498ee98596f4 Mon Sep 17 00:00:00 2001 From: Robin Pringle Date: Wed, 28 Jun 2023 21:02:33 +0000 Subject: [PATCH 272/598] Update Afrikaans translation --- locales/af.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/af.json b/locales/af.json index 4db98a8b..35f40a13 100644 --- a/locales/af.json +++ b/locales/af.json @@ -10,5 +10,6 @@ "LIVE": "LEWENDIG", "generic_subscribers_count": "{{count}} intekenaar", "generic_subscribers_count_plural": "{{count}} intekenare", - "Shared `x` ago": "`x` gelede gedeel" + "Shared `x` ago": "`x` gelede gedeel", + "New passwords must match": "Nuwe wagwoord moet ooreenstem" } From 1647092b3c77ac271f4b50a3ee9bdfd3d6be4345 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 1 Jul 2023 19:29:24 +0200 Subject: [PATCH 273/598] Config: Make 'hmac_key' mandatory --- src/invidious/config.cr | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/invidious/config.cr b/src/invidious/config.cr index 9fc58409..7030c925 100644 --- a/src/invidious/config.cr +++ b/src/invidious/config.cr @@ -85,7 +85,7 @@ class Config # Used to tell Invidious it is behind a proxy, so links to resources should be https:// property https_only : Bool? # HMAC signing key for CSRF tokens and verifying pubsub subscriptions - property hmac_key : String? + property hmac_key : String = "" # Domain to be used for links to resources on the site where an absolute URL is required property domain : String? # Subscribe to channels using PubSubHubbub (requires domain, hmac_key) @@ -204,6 +204,13 @@ class Config end {% end %} + # HMAC_key is mandatory + # See: https://github.com/iv-org/invidious/issues/3854 + if config.hmac_key.empty? + puts "Config: 'hmac_key' is required/can't be empty" + exit(1) + end + # Build database_url from db.* if it's not set directly if config.database_url.to_s.empty? if db = config.db @@ -216,7 +223,7 @@ class Config path: db.dbname, ) else - puts "Config : Either database_url or db.* is required" + puts "Config: Either database_url or db.* is required" exit(1) end end From f64e311dcd656b3552b21b7bd3998d82bc8da900 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 1 Jul 2023 19:29:40 +0200 Subject: [PATCH 274/598] Config: Update example config documentation --- config/config.example.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/config/config.example.yml b/config/config.example.yml index c591eb6a..2da6e55e 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -455,13 +455,17 @@ jobs: #use_pubsub_feeds: false ## -## HMAC signing key used for CSRF tokens and pubsub +## HMAC signing key used for CSRF tokens, cookies and pubsub ## subscriptions verification. ## +## Note: This parameter is mandatory and should be a random string. +## Such random string can be generated on linux with the following +## command: `pwdgen 20 1` +## ## Accepted values: a string ## Default: ## -#hmac_key: +hmac_key: "CHANGE_ME!!" ## ## List of video IDs where the "download" widget must be From ba43365acb20ee4fe1b94e9457595fa6e30ae8f9 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 1 Jul 2023 19:38:50 +0200 Subject: [PATCH 275/598] Config: Stop if 'hmac_key' is the default value --- src/invidious/config.cr | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/invidious/config.cr b/src/invidious/config.cr index 7030c925..e5f1e822 100644 --- a/src/invidious/config.cr +++ b/src/invidious/config.cr @@ -209,6 +209,9 @@ class Config if config.hmac_key.empty? puts "Config: 'hmac_key' is required/can't be empty" exit(1) + elsif config.hmac_key == "CHANGE_ME!!" + puts "Config: The value of 'hmac_key' needs to be changed!!" + exit(1) end # Build database_url from db.* if it's not set directly From e2a6f5ddf26f7fca4ffe9be867dd15a3ed5f73b0 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 1 Jul 2023 19:40:28 +0200 Subject: [PATCH 276/598] Docker: Add 'hmac_key' to docker-compose.yml --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index eb83b020..6a854475 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -30,6 +30,7 @@ services: # domain: # https_only: false # statistics_enabled: false + hmac_key: "CHANGE_ME!!" healthcheck: test: wget -nv --tries=1 --spider http://127.0.0.1:3000/api/v1/comments/jNQXAC9IVRw || exit 1 interval: 30s From d7568ac45a77323e724d0664d8959d9c1e8fa04c Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 1 Jul 2023 21:53:56 +0200 Subject: [PATCH 277/598] Remove old warning code about unconfigured 'hmac_key' --- src/invidious.cr | 9 ++------- src/invidious/views/template.ecr | 8 -------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index 636e28a6..84e1895d 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -57,9 +57,8 @@ end # Simple alias to make code easier to read alias IV = Invidious -CONFIG = Config.load -HMAC_KEY_CONFIGURED = CONFIG.hmac_key != nil -HMAC_KEY = CONFIG.hmac_key || Random::Secure.hex(32) +CONFIG = Config.load +HMAC_KEY = CONFIG.hmac_key PG_DB = DB.open CONFIG.database_url ARCHIVE_URL = URI.parse("https://archive.org") @@ -230,10 +229,6 @@ Kemal.config.host_binding = Kemal.config.host_binding != "0.0.0.0" ? Kemal.confi Kemal.config.port = Kemal.config.port != 3000 ? Kemal.config.port : CONFIG.port Kemal.config.app_name = "Invidious" -if !HMAC_KEY_CONFIGURED - LOGGER.warn("Please configure hmac_key by July 1st, see more here: https://github.com/iv-org/invidious/issues/3854") -end - # Use in kemal's production mode. # Users can also set the KEMAL_ENV environmental variable for this to be set automatically. {% if flag?(:release) || flag?(:production) %} diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr index aa0fc15f..77265679 100644 --- a/src/invidious/views/template.ecr +++ b/src/invidious/views/template.ecr @@ -111,14 +111,6 @@
    <% end %> - <% if env.get? "user" %> - <% if !HMAC_KEY_CONFIGURED && CONFIG.admins.includes? env.get("user").as(Invidious::User).email %> -
    -

    Message for admin: please configure hmac_key, see more here.

    -
    - <% end %> - <% end %> - <%= content %>
    From a38edd733002166d334261abb39a220c8972ca25 Mon Sep 17 00:00:00 2001 From: Omer Naveed Date: Sat, 1 Jul 2023 12:29:02 -0500 Subject: [PATCH 278/598] Fix Nil assertion failed in RSS feeds --- src/invidious/routes/feeds.cr | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/invidious/routes/feeds.cr b/src/invidious/routes/feeds.cr index fc62c5a3..60f8db05 100644 --- a/src/invidious/routes/feeds.cr +++ b/src/invidious/routes/feeds.cr @@ -154,20 +154,26 @@ module Invidious::Routes::Feeds return error_atom(500, ex) end + namespaces = { + "yt" => "http://www.youtube.com/xml/schemas/2015", + "media" => "http://search.yahoo.com/mrss/", + "default" => "http://www.w3.org/2005/Atom", + } + response = YT_POOL.client &.get("/feeds/videos.xml?channel_id=#{channel.ucid}") - rss = XML.parse_html(response.body) + rss = XML.parse(response.body) - videos = rss.xpath_nodes("//feed/entry").map do |entry| - video_id = entry.xpath_node("videoid").not_nil!.content - title = entry.xpath_node("title").not_nil!.content + videos = rss.xpath_nodes("//default:feed/default:entry", namespaces).map do |entry| + video_id = entry.xpath_node("yt:videoId", namespaces).not_nil!.content + title = entry.xpath_node("default:title", namespaces).not_nil!.content - published = Time.parse_rfc3339(entry.xpath_node("published").not_nil!.content) - updated = Time.parse_rfc3339(entry.xpath_node("updated").not_nil!.content) + published = Time.parse_rfc3339(entry.xpath_node("default:published", namespaces).not_nil!.content) + updated = Time.parse_rfc3339(entry.xpath_node("default:updated", namespaces).not_nil!.content) - author = entry.xpath_node("author/name").not_nil!.content - ucid = entry.xpath_node("channelid").not_nil!.content - description_html = entry.xpath_node("group/description").not_nil!.to_s - views = entry.xpath_node("group/community/statistics").not_nil!.["views"].to_i64 + author = entry.xpath_node("default:author/default:name", namespaces).not_nil!.content + ucid = entry.xpath_node("yt:channelId", namespaces).not_nil!.content + description_html = entry.xpath_node("media:group/media:description", namespaces).not_nil!.to_s + views = entry.xpath_node("media:group/media:community/media:statistics", namespaces).not_nil!.["views"].to_i64 SearchVideo.new({ title: title, From 4a92dce449138cf6d6d88021de5bee5ee8388cc6 Mon Sep 17 00:00:00 2001 From: Jason Thatcher Date: Tue, 4 Jul 2023 16:18:30 +1000 Subject: [PATCH 279/598] config.example.yml: Fix typo in pwgen command (#3965) `pwdgen` -> `pwgen`. --- config/config.example.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.example.yml b/config/config.example.yml index 2da6e55e..34070fe5 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -460,7 +460,7 @@ jobs: ## ## Note: This parameter is mandatory and should be a random string. ## Such random string can be generated on linux with the following -## command: `pwdgen 20 1` +## command: `pwgen 20 1` ## ## Accepted values: a string ## Default: From 507bed6313b49564e53b69a5c9b4d072d1e05e4b Mon Sep 17 00:00:00 2001 From: Orville Date: Wed, 5 Jul 2023 09:13:05 -0400 Subject: [PATCH 280/598] Workaround for https://github.com/iv-org/invidious/issues/3909 (#3967) --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 929b11e1..d4657792 100644 --- a/Makefile +++ b/Makefile @@ -86,6 +86,7 @@ clean: distclean: clean rm -rf libs + rm -rf ~/.cache/{crystal,shards} # ----------------------- From 0ba22ef391a7b350d139dfd256aa20a7e1f812ed Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Tue, 18 Apr 2023 00:04:49 +0200 Subject: [PATCH 281/598] I18n: Add a function to determine if a given locale is RTL --- src/invidious/helpers/i18n.cr | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/invidious/helpers/i18n.cr b/src/invidious/helpers/i18n.cr index a9ed1f64..76e477a4 100644 --- a/src/invidious/helpers/i18n.cr +++ b/src/invidious/helpers/i18n.cr @@ -165,3 +165,12 @@ def translate_bool(locale : String?, translation : Bool) return translate(locale, "No") end end + +def locale_is_rtl?(locale : String?) + # Fallback to en-US + return false if locale.nil? + + # Arabic, Persian, Hebrew + # See https://en.wikipedia.org/wiki/Right-to-left_script#List_of_RTL_scripts + return {"ar", "fa", "he"}.includes? locale +end From 462609d90d38ec8e9aada1d700cfbca46e906552 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Wed, 26 Apr 2023 22:30:13 +0200 Subject: [PATCH 282/598] Utils: Create a function to append parameters to a base URL --- src/invidious/http_server/utils.cr | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/invidious/http_server/utils.cr b/src/invidious/http_server/utils.cr index e3f1fa0f..222dfc4a 100644 --- a/src/invidious/http_server/utils.cr +++ b/src/invidious/http_server/utils.cr @@ -1,3 +1,5 @@ +require "uri" + module Invidious::HttpServer module Utils extend self @@ -16,5 +18,23 @@ module Invidious::HttpServer return "#{url.request_target}?#{params}" end end + + def add_params_to_url(url : String | URI, params : URI::Params) : URI + url = URI.parse(url) if url.is_a?(String) + + url_query = url.query || "" + + # Append the parameters + url.query = String.build do |str| + if !url_query.empty? + str << url_query + str << '&' + end + + str << params + end + + return url + end end end From c0887497447a24cad1f1e8b8268b8ccfbc78ae77 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Mon, 17 Apr 2023 21:25:00 +0200 Subject: [PATCH 283/598] HTML: Add code to generate page nav buttons --- src/invidious/frontend/pagination.cr | 97 ++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 src/invidious/frontend/pagination.cr diff --git a/src/invidious/frontend/pagination.cr b/src/invidious/frontend/pagination.cr new file mode 100644 index 00000000..3f931f4e --- /dev/null +++ b/src/invidious/frontend/pagination.cr @@ -0,0 +1,97 @@ +require "uri" + +module Invidious::Frontend::Pagination + extend self + + private def previous_page(str : String::Builder, locale : String?, url : String) + # Link + str << %() + + if locale_is_rtl?(locale) + # Inverted arrow ("previous" points to the right) + str << translate(locale, "Previous page") + str << "  " + str << %() + else + # Regular arrow ("previous" points to the left) + str << %() + str << "  " + str << translate(locale, "Previous page") + end + + str << "" + end + + private def next_page(str : String::Builder, locale : String?, url : String) + # Link + str << %() + + if locale_is_rtl?(locale) + # Inverted arrow ("next" points to the left) + str << %() + str << "  " + str << translate(locale, "Next page") + else + # Regular arrow ("next" points to the right) + str << translate(locale, "Next page") + str << "  " + str << %() + end + + str << "" + end + + def nav_numeric(locale : String?, *, base_url : String | URI, current_page : Int, show_next : Bool = true) + return String.build do |str| + str << %(
    \n) + str << %(\n) + str << %(
    \n\n) + end + end + + def nav_ctoken(locale : String?, *, base_url : String | URI, ctoken : String?) + return String.build do |str| + str << %(
    \n) + str << %(\n) + str << %(
    \n\n) + end + end +end From 57c7b922f7c3cd04d08bb6be9793464d31213fb1 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Mon, 17 Apr 2023 21:26:04 +0200 Subject: [PATCH 284/598] HTML: Make a dedicated ECR component for items + pagination --- src/invidious/views/components/items_paginated.ecr | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/invidious/views/components/items_paginated.ecr diff --git a/src/invidious/views/components/items_paginated.ecr b/src/invidious/views/components/items_paginated.ecr new file mode 100644 index 00000000..c82b1772 --- /dev/null +++ b/src/invidious/views/components/items_paginated.ecr @@ -0,0 +1,11 @@ +<%= page_nav_html %> + +
    + <%- videos.each do |item| -%> + <%= rendered "components/item" %> + <%- end -%> +
    + +<%= page_nav_html %> + + From 77d401cec257b1f8b1b5c233134789441083fcdc Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Thu, 20 Apr 2023 18:55:35 +0200 Subject: [PATCH 285/598] CSS: add styling for the new buttons --- assets/css/default.css | 54 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/assets/css/default.css b/assets/css/default.css index 431a0427..eb90c09c 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -115,6 +115,11 @@ div { padding-right: 10px; } + +/* + * Buttons + */ + body a.pure-button { color: rgba(0,0,0,.8); } @@ -127,14 +132,36 @@ body a.pure-button-primary, color: rgba(35, 35, 35, 1); } +.pure-button-primary, +.pure-button-secondary { + border: 1px solid #a0a0a0; + border-radius: 3px; + margin: 0 .4em; +} + +.dark-theme .pure-button-secondary { + background-color: #0002; + color: #ddd; +} + button.pure-button-primary:hover, -body a.pure-button-primary:hover, button.pure-button-primary:focus, +body a.pure-button-primary:hover, body a.pure-button-primary:focus { background-color: rgba(0, 182, 240, 1); color: #fff; } +button.pure-button-secondary:hover, +button.pure-button-secondary:focus { + border-color: rgba(0, 182, 240, 1); +} + + +/* + * Thumbnails + */ + div.thumbnail { padding: 28.125%; position: relative; @@ -192,6 +219,7 @@ div.watched-indicator { top: -0.7em; } + /* * Navbar */ @@ -347,6 +375,22 @@ p.video-data { margin: 0; font-weight: bold; font-size: 80%; } border: none; } + +/* + * Page navigation + */ + +.page-nav-container { margin: 15px 0 30px 0; } + +.page-prev-container { text-align: start; } +.page-next-container { text-align: end; } + +.page-prev-container, +.page-next-container { + display: inline-block; +} + + /* * Footer */ @@ -389,6 +433,7 @@ span > select { word-wrap: normal; } + /* * Light theme */ @@ -453,6 +498,7 @@ span > select { } } + /* * Dark theme */ @@ -539,6 +585,12 @@ body.dark-theme { } } + +/* + * Miscellanous + */ + + /*With commit d9528f5 all contents of the page is now within a flexbox. However, the hr element is rendered improperly within one. See https://stackoverflow.com/a/34372979 for more info */ From c4ef3bed9556700c4c4e8c02c394d16fd3aae03d Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Tue, 18 Apr 2023 00:04:01 +0200 Subject: [PATCH 286/598] HTML: Use the new pagination component for playlists --- src/invidious/routes/playlists.cr | 22 ++++++++++++++++ src/invidious/views/add_playlist_items.ecr | 30 +--------------------- src/invidious/views/edit_playlist.ecr | 25 +----------------- src/invidious/views/playlist.ecr | 25 +----------------- 4 files changed, 25 insertions(+), 77 deletions(-) diff --git a/src/invidious/routes/playlists.cr b/src/invidious/routes/playlists.cr index 1dd3f32e..604fe4e1 100644 --- a/src/invidious/routes/playlists.cr +++ b/src/invidious/routes/playlists.cr @@ -170,6 +170,13 @@ module Invidious::Routes::Playlists csrf_token = generate_response(sid, {":edit_playlist"}, HMAC_KEY) + # Pagination + page_nav_html = Frontend::Pagination.nav_numeric(locale, + base_url: "/playlist?list=#{playlist.id}", + current_page: page, + show_next: (videos.size == 100) + ) + templated "edit_playlist" end @@ -252,6 +259,14 @@ module Invidious::Routes::Playlists videos = [] of SearchVideo end + # Pagination + query_encoded = URI.encode_www_form(query.try &.text || "", space_to_plus: true) + page_nav_html = Frontend::Pagination.nav_numeric(locale, + base_url: "/add_playlist_items?list=#{playlist.id}&q=#{query_encoded}", + current_page: page, + show_next: (videos.size >= 20) + ) + env.set "add_playlist_items", plid templated "add_playlist_items" end @@ -427,6 +442,13 @@ module Invidious::Routes::Playlists env.set "remove_playlist_items", plid end + # Pagination + page_nav_html = Frontend::Pagination.nav_numeric(locale, + base_url: "/playlist?list=#{playlist.id}", + current_page: page, + show_next: (page_count != 1 && page < page_count) + ) + templated "playlist" end diff --git a/src/invidious/views/add_playlist_items.ecr b/src/invidious/views/add_playlist_items.ecr index bcba74cf..6aea82ae 100644 --- a/src/invidious/views/add_playlist_items.ecr +++ b/src/invidious/views/add_playlist_items.ecr @@ -31,33 +31,5 @@ -
    - <% videos.each_slice(4) do |slice| %> - <% slice.each do |item| %> - <%= rendered "components/item" %> - <% end %> - <% end %> -
    - - -<% if query %> - <%- query_encoded = URI.encode_www_form(query.text, space_to_plus: true) -%> -
    -
    - <% if query.page > 1 %> - - <%= translate(locale, "Previous page") %> - - <% end %> -
    -
    -
    - <% if videos.size >= 20 %> - - <%= translate(locale, "Next page") %> - - <% end %> -
    -
    -<% end %> +<%= rendered "components/items_paginated" %> diff --git a/src/invidious/views/edit_playlist.ecr b/src/invidious/views/edit_playlist.ecr index 548104c8..d2981886 100644 --- a/src/invidious/views/edit_playlist.ecr +++ b/src/invidious/views/edit_playlist.ecr @@ -56,28 +56,5 @@
    -
    -<% videos.each do |item| %> - <%= rendered "components/item" %> -<% end %> -
    - - -
    -
    - <% if page > 1 %> - - <%= translate(locale, "Previous page") %> - - <% end %> -
    -
    -
    - <% if videos.size == 100 %> - - <%= translate(locale, "Next page") %> - - <% end %> -
    -
    +<%= rendered "components/items_paginated" %> diff --git a/src/invidious/views/playlist.ecr b/src/invidious/views/playlist.ecr index a04acf4c..08995a83 100644 --- a/src/invidious/views/playlist.ecr +++ b/src/invidious/views/playlist.ecr @@ -100,28 +100,5 @@ <% end %> -
    -<% videos.each do |item| %> - <%= rendered "components/item" %> -<% end %> -
    - - -
    -
    - <% if page > 1 %> - - <%= translate(locale, "Previous page") %> - - <% end %> -
    -
    -
    - <% if page_count != 1 && page < page_count %> - - <%= translate(locale, "Next page") %> - - <% end %> -
    -
    +<%= rendered "components/items_paginated" %> From efaf7cb09c8aad606d59cacab71c4a0a269d785b Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Tue, 18 Apr 2023 00:12:56 +0200 Subject: [PATCH 287/598] HTML: Use the new pagination component for search results --- src/invidious/routes/search.cr | 22 +++++++++++++-------- src/invidious/views/hashtag.ecr | 35 +-------------------------------- src/invidious/views/search.ecr | 35 +-------------------------------- 3 files changed, 16 insertions(+), 76 deletions(-) diff --git a/src/invidious/routes/search.cr b/src/invidious/routes/search.cr index 6c3088de..edf0351c 100644 --- a/src/invidious/routes/search.cr +++ b/src/invidious/routes/search.cr @@ -59,17 +59,21 @@ module Invidious::Routes::Search return error_template(500, ex) end - params = query.to_http_params - url_prev_page = "/search?#{params}&page=#{query.page - 1}" - url_next_page = "/search?#{params}&page=#{query.page + 1}" - redirect_url = Invidious::Frontend::Misc.redirect_url(env) + # Pagination + page_nav_html = Frontend::Pagination.nav_numeric(locale, + base_url: "/search?#{query.to_http_params}", + current_page: query.page, + show_next: (videos.size >= 20) + ) + 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 @@ -96,11 +100,13 @@ module Invidious::Routes::Search return error_template(500, ex) end - params = env.params.query.empty? ? "" : "&#{env.params.query}" - + # Pagination hashtag_encoded = URI.encode_www_form(hashtag, space_to_plus: false) - url_prev_page = "/hashtag/#{hashtag_encoded}?page=#{page - 1}#{params}" - url_next_page = "/hashtag/#{hashtag_encoded}?page=#{page + 1}#{params}" + page_nav_html = Frontend::Pagination.nav_numeric(locale, + base_url: "/hashtag/#{hashtag_encoded}", + current_page: page, + show_next: (videos.size >= 60) + ) templated "hashtag" end diff --git a/src/invidious/views/hashtag.ecr b/src/invidious/views/hashtag.ecr index 3351c21c..2000337e 100644 --- a/src/invidious/views/hashtag.ecr +++ b/src/invidious/views/hashtag.ecr @@ -4,38 +4,5 @@
    -
    -
    - <%- if page > 1 -%> - <%= translate(locale, "Previous page") %> - <%- end -%> -
    -
    -
    - <%- if videos.size >= 60 -%> - <%= translate(locale, "Next page") %> - <%- end -%> -
    -
    -
    - <%- videos.each do |item| -%> - <%= rendered "components/item" %> - <%- end -%> -
    - - - -
    -
    - <%- if page > 1 -%> - <%= translate(locale, "Previous page") %> - <%- end -%> -
    -
    -
    - <%- if videos.size >= 60 -%> - <%= translate(locale, "Next page") %> - <%- end -%> -
    -
    +<%= rendered "components/items_paginated" %> diff --git a/src/invidious/views/search.ecr b/src/invidious/views/search.ecr index a7469e36..627a13b0 100644 --- a/src/invidious/views/search.ecr +++ b/src/invidious/views/search.ecr @@ -7,19 +7,6 @@ <%= Invidious::Frontend::SearchFilters.generate(query.filters, query.text, query.page, locale) %>
    -
    -
    - <%- if query.page > 1 -%> - <%= translate(locale, "Previous page") %> - <%- end -%> -
    -
    -
    - <%- if videos.size >= 20 -%> - <%= translate(locale, "Next page") %> - <%- end -%> -
    -
    <%- if videos.empty? -%>
    @@ -30,25 +17,5 @@
    <%- else -%> -
    - <%- videos.each do |item| -%> - <%= rendered "components/item" %> - <%- end -%> -
    + <%= rendered "components/items_paginated" %> <%- end -%> - - - -
    -
    - <%- if query.page > 1 -%> - <%= translate(locale, "Previous page") %> - <%- end -%> -
    -
    -
    - <%- if videos.size >= 20 -%> - <%= translate(locale, "Next page") %> - <%- end -%> -
    -
    From 7bd6d0ac4961e7f2433eb3268a45b78642229896 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Fri, 21 Apr 2023 00:28:11 +0200 Subject: [PATCH 288/598] HTML: Use the new pagination component for channel pages --- src/invidious/routes/playlists.cr | 14 +++++------ src/invidious/routes/search.cr | 8 +++--- src/invidious/views/channel.ecr | 25 ++++++------------- .../views/components/items_paginated.ecr | 2 +- src/invidious/views/search.ecr | 2 +- 5 files changed, 20 insertions(+), 31 deletions(-) diff --git a/src/invidious/routes/playlists.cr b/src/invidious/routes/playlists.cr index 604fe4e1..5cb96809 100644 --- a/src/invidious/routes/playlists.cr +++ b/src/invidious/routes/playlists.cr @@ -163,9 +163,9 @@ module Invidious::Routes::Playlists end begin - videos = get_playlist_videos(playlist, offset: (page - 1) * 100) + items = get_playlist_videos(playlist, offset: (page - 1) * 100) rescue ex - videos = [] of PlaylistVideo + items = [] of PlaylistVideo end csrf_token = generate_response(sid, {":edit_playlist"}, HMAC_KEY) @@ -174,7 +174,7 @@ module Invidious::Routes::Playlists page_nav_html = Frontend::Pagination.nav_numeric(locale, base_url: "/playlist?list=#{playlist.id}", current_page: page, - show_next: (videos.size == 100) + show_next: (items.size == 100) ) templated "edit_playlist" @@ -254,9 +254,9 @@ module Invidious::Routes::Playlists begin query = Invidious::Search::Query.new(env.params.query, :playlist, region) - videos = query.process.select(SearchVideo).map(&.as(SearchVideo)) + items = query.process.select(SearchVideo).map(&.as(SearchVideo)) rescue ex - videos = [] of SearchVideo + items = [] of SearchVideo end # Pagination @@ -264,7 +264,7 @@ module Invidious::Routes::Playlists page_nav_html = Frontend::Pagination.nav_numeric(locale, base_url: "/add_playlist_items?list=#{playlist.id}&q=#{query_encoded}", current_page: page, - show_next: (videos.size >= 20) + show_next: (items.size >= 20) ) env.set "add_playlist_items", plid @@ -433,7 +433,7 @@ module Invidious::Routes::Playlists end begin - videos = get_playlist_videos(playlist, offset: (page - 1) * 200) + items = get_playlist_videos(playlist, offset: (page - 1) * 200) rescue ex return error_template(500, "Error encountered while retrieving playlist videos.
    #{ex.message}") end diff --git a/src/invidious/routes/search.cr b/src/invidious/routes/search.cr index edf0351c..5be33533 100644 --- a/src/invidious/routes/search.cr +++ b/src/invidious/routes/search.cr @@ -52,7 +52,7 @@ module Invidious::Routes::Search user = env.get? "user" begin - videos = query.process + items = query.process rescue ex : ChannelSearchException return error_template(404, "Unable to find channel with id of '#{HTML.escape(ex.channel)}'. Are you sure that's an actual channel id? It should look like 'UC4QobU6STFB0P71PMvOGN5A'.") rescue ex @@ -65,7 +65,7 @@ module Invidious::Routes::Search page_nav_html = Frontend::Pagination.nav_numeric(locale, base_url: "/search?#{query.to_http_params}", current_page: query.page, - show_next: (videos.size >= 20) + show_next: (items.size >= 20) ) if query.type == Invidious::Search::Query::Type::Channel @@ -95,7 +95,7 @@ module Invidious::Routes::Search end begin - videos = Invidious::Hashtag.fetch(hashtag, page) + items = Invidious::Hashtag.fetch(hashtag, page) rescue ex return error_template(500, ex) end @@ -105,7 +105,7 @@ module Invidious::Routes::Search page_nav_html = Frontend::Pagination.nav_numeric(locale, base_url: "/hashtag/#{hashtag_encoded}", current_page: page, - show_next: (videos.size >= 60) + show_next: (items.size >= 60) ) templated "hashtag" diff --git a/src/invidious/views/channel.ecr b/src/invidious/views/channel.ecr index 6e62a471..91fe40b9 100644 --- a/src/invidious/views/channel.ecr +++ b/src/invidious/views/channel.ecr @@ -15,7 +15,12 @@ youtube_url = "https://www.youtube.com#{relative_url}" redirect_url = Invidious::Frontend::Misc.redirect_url(env) --%> + + page_nav_html = IV::Frontend::Pagination.nav_ctoken(locale, + base_url: relative_url, + ctoken: next_continuation + ) +%> <% content_for "header" do %> <%- if selected_tab.videos? -%> @@ -43,21 +48,5 @@
    -
    -<% items.each do |item| %> - <%= rendered "components/item" %> -<% end %> -
    - - -
    -
    -
    - <% if next_continuation %> - - <%= translate(locale, "Next page") %> - - <% end %> -
    -
    +<%= rendered "components/items_paginated" %> diff --git a/src/invidious/views/components/items_paginated.ecr b/src/invidious/views/components/items_paginated.ecr index c82b1772..4534a0a3 100644 --- a/src/invidious/views/components/items_paginated.ecr +++ b/src/invidious/views/components/items_paginated.ecr @@ -1,7 +1,7 @@ <%= page_nav_html %>
    - <%- videos.each do |item| -%> + <%- items.each do |item| -%> <%= rendered "components/item" %> <%- end -%>
    diff --git a/src/invidious/views/search.ecr b/src/invidious/views/search.ecr index 627a13b0..b1300214 100644 --- a/src/invidious/views/search.ecr +++ b/src/invidious/views/search.ecr @@ -8,7 +8,7 @@
    -<%- if videos.empty? -%> +<%- if items.empty? -%>
    <%= translate(locale, "search_message_no_results") %>

    From b6bbfb9b200fc920854ce91835026da0fd6552db Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 22 Apr 2023 12:58:46 +0200 Subject: [PATCH 289/598] HTML: Use new buttons for thumbnail overlays In addition, this commit also heavily changes the structure of the generic "video card" item. Main benefits: * Improved accessibility for keyboard users * Many styling glitches were fixed * PlaylistVideos now use the same items as the rest * Elements all have distinct CSS classes * Design can be expanded to add more icons --- assets/css/default.css | 51 ++++---- src/invidious/views/components/item.ecr | 157 ++++++++++-------------- src/invidious/views/feeds/history.ecr | 8 +- 3 files changed, 98 insertions(+), 118 deletions(-) diff --git a/assets/css/default.css b/assets/css/default.css index eb90c09c..48cb4264 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -152,9 +152,15 @@ body a.pure-button-primary:focus { color: #fff; } -button.pure-button-secondary:hover, -button.pure-button-secondary:focus { - border-color: rgba(0, 182, 240, 1); +.pure-button-secondary:hover, +.pure-button-secondary:focus { + color: rgb(0, 182, 240); + border-color: rgb(0, 182, 240); +} + +.pure-button-secondary.low-profile { + padding: 5px 10px; + margin: 0; } @@ -163,21 +169,19 @@ button.pure-button-secondary:focus { */ div.thumbnail { - padding: 28.125%; position: relative; + width: 100%; box-sizing: border-box; } img.thumbnail { - position: absolute; + display: block; /* See: https://stackoverflow.com/a/11635197 */ width: 100%; - height: 100%; - left: 0; - top: 0; object-fit: cover; } div.watched-overlay { + z-index: 50; position: absolute; top: 0; left: 0; @@ -195,28 +199,27 @@ div.watched-indicator { background-color: red; } -.length { +div.thumbnail > .top-left-overlay, +div.thumbnail > .bottom-right-overlay { z-index: 100; position: absolute; - background-color: rgba(35, 35, 35, 0.75); - color: #fff; - border-radius: 2px; - padding: 2px; + padding: 0; + margin: 0; font-size: 16px; - right: 0.25em; - bottom: -0.75em; } -.watched { - z-index: 100; - position: absolute; - background-color: rgba(35, 35, 35, 0.75); +.top-left-overlay { top: 0.6em; left: 0.6em; } +.bottom-right-overlay { bottom: 0.6em; right: 0.6em; } + +.length { + padding: 1px; + margin: -2px 0; color: #fff; - border-radius: 2px; - padding: 4px 8px 4px 8px; - font-size: 16px; - left: 0.2em; - top: -0.7em; + border-radius: 3px; +} + +.length, .top-left-overlay button { + background-color: rgba(35, 35, 35, 0.85); } diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr index 7cfd38db..f05e1338 100644 --- a/src/invidious/views/components/item.ecr +++ b/src/invidious/views/components/item.ecr @@ -7,7 +7,7 @@ <% if !env.get("preferences").as(Preferences).thin_mode %>
    - " alt="" /> + " alt="" />
    <% end %>

    <%= HTML.escape(item.author) %><% if !item.author_verified.nil? && item.author_verified %> <% end %>

    @@ -25,7 +25,7 @@
    <% if !env.get("preferences").as(Preferences).thin_mode %>
    - " alt="" /> + " alt="" />

    <%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %>

    <% end %> @@ -38,7 +38,7 @@
    <% if !env.get("preferences").as(Preferences).thin_mode %>
    - + <% if item.length_seconds != 0 %>

    <%= recode_length_seconds(item.length_seconds) %>

    <% end %> @@ -54,104 +54,79 @@

    <%= HTML.escape(item.author) %>

    - <% when PlaylistVideo %> - - <% if !env.get("preferences").as(Preferences).thin_mode %> -
    - - - <% if plid_form = env.get?("remove_playlist_items") %> - " method="post"> - "> -

    - -

    - - <% end %> - - <% if item.responds_to?(:live_now) && item.live_now %> -

    <%= translate(locale, "LIVE") %>

    - <% elsif item.length_seconds != 0 %> -

    <%= recode_length_seconds(item.length_seconds) %>

    - <% end %> - - <% if item_watched %> -
    -
    - <% end %> -
    - <% end %> -

    <%= HTML.escape(item.title) %>

    -
    - -
    - - <% endpoint_params = "?v=#{item.id}&list=#{item.plid}" %> - <%= rendered "components/video-context-buttons" %> -
    - -
    -
    - <% if item.responds_to?(:premiere_timestamp) && item.premiere_timestamp.try &.> Time.utc %> -

    <%= translate(locale, "Premieres in `x`", recode_date((item.premiere_timestamp.as(Time) - Time.utc).ago, locale)) %>

    - <% elsif Time.utc - item.published > 1.minute %> -

    <%= translate(locale, "Shared `x` ago", recode_date(item.published, locale)) %>

    - <% end %> -
    - - <% if item.responds_to?(:views) && item.views %> -
    -

    <%= translate_count(locale, "generic_views_count", item.views || 0, NumberFormatting::Short) %>

    -
    - <% end %> -
    <% when Category %> <% else %> - - <% if !env.get("preferences").as(Preferences).thin_mode %> -
    - - <% if env.get? "show_watched" %> -
    " method="post"> - "> -

    - -

    -
    - <% elsif plid_form = env.get? "add_playlist_items" %> -
    " method="post"> - "> -

    - -

    -
    - <% end %> + <%- + # `endpoint_params` is used for the "video-context-buttons" component + if item.is_a?(PlaylistVideo) + link_url = "/watch?v=#{item.id}&list=#{item.plid}&index=#{item.index}" + endpoint_params = "?v=#{item.id}&list=#{item.plid}" + else + link_url = "/watch?v=#{item.id}" + endpoint_params = "?v=#{item.id}" + end + -%> - <% if item.responds_to?(:live_now) && item.live_now %> -

    <%= translate(locale, "LIVE") %>

    - <% elsif item.length_seconds != 0 %> -

    <%= recode_length_seconds(item.length_seconds) %>

    - <% end %> +
    + <%- if !env.get("preferences").as(Preferences).thin_mode -%> + + + + <%- end -%> - <% if item_watched %> -
    -
    - <% end %> -
    +
    + <%- if env.get? "show_watched" -%> +
    " method="post"> + "> + +
    + <%- end -%> + + <%- if plid_form = env.get?("add_playlist_items") -%> + <%- form_parameters = "action_add_video=1&video_id=#{item.id}&playlist_id=#{plid_form}&referer=#{env.get("current_page")}" -%> +
    + "> + +
    + <%- elsif item.is_a?(PlaylistVideo) && (plid_form = env.get?("remove_playlist_items")) -%> + <%- form_parameters = "action_remove_video=1&set_video_id=#{item.index}&playlist_id=#{plid_form}&referer=#{env.get("current_page")}" -%> +
    + "> + +
    + <%- end -%> +
    + +
    + <%- if item.responds_to?(:live_now) && item.live_now -%> +

     <%= translate(locale, "LIVE") %>

    + <%- elsif item.length_seconds != 0 -%> +

    <%= recode_length_seconds(item.length_seconds) %>

    + <%- end -%> +
    + + <% if item_watched %> +
    +
    <% end %> -

    <%= HTML.escape(item.title) %>

    - +
    + + diff --git a/src/invidious/views/feeds/history.ecr b/src/invidious/views/feeds/history.ecr index 2234b297..5301a232 100644 --- a/src/invidious/views/feeds/history.ecr +++ b/src/invidious/views/feeds/history.ecr @@ -35,12 +35,14 @@ <% if !env.get("preferences").as(Preferences).thin_mode %>
    + +
    " method="post"> "> -

    - -

    +
    +

    <% end %> From 080c7446c6c26c5d8670107cf4161ba4609e5e4a Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 22 Apr 2023 17:50:34 +0200 Subject: [PATCH 290/598] HTML: Use new buttons for playlists (save/delete/add videos/etc...) --- assets/css/default.css | 2 +- locales/en-US.json | 6 +++ locales/fr.json | 6 +++ src/invidious/views/components/item.ecr | 10 ++-- src/invidious/views/edit_playlist.ecr | 64 +++++++++++----------- src/invidious/views/playlist.ecr | 70 ++++++++++++++++--------- 6 files changed, 94 insertions(+), 64 deletions(-) diff --git a/assets/css/default.css b/assets/css/default.css index 48cb4264..7a99a0db 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -219,7 +219,7 @@ div.thumbnail > .bottom-right-overlay { } .length, .top-left-overlay button { - background-color: rgba(35, 35, 35, 0.85); + background-color: rgba(35, 35, 35, 0.85) !important; } diff --git a/locales/en-US.json b/locales/en-US.json index e13ba968..c41a631a 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -9,6 +9,11 @@ "generic_subscribers_count_plural": "{{count}} subscribers", "generic_subscriptions_count": "{{count}} subscription", "generic_subscriptions_count_plural": "{{count}} subscriptions", + "generic_button_delete": "Delete", + "generic_button_edit": "Edit", + "generic_button_save": "Save", + "generic_button_cancel": "Cancel", + "generic_button_rss": "RSS", "LIVE": "LIVE", "Shared `x` ago": "Shared `x` ago", "Unsubscribe": "Unsubscribe", @@ -170,6 +175,7 @@ "Title": "Title", "Playlist privacy": "Playlist privacy", "Editing playlist `x`": "Editing playlist `x`", + "playlist_button_add_items": "Add videos", "Show more": "Show more", "Show less": "Show less", "Watch on YouTube": "Watch on YouTube", diff --git a/locales/fr.json b/locales/fr.json index d2607a49..2eb4dd2b 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -9,6 +9,11 @@ "generic_subscribers_count_plural": "{{count}} abonnés", "generic_subscriptions_count": "{{count}} abonnement", "generic_subscriptions_count_plural": "{{count}} abonnements", + "generic_button_delete": "Supprimer", + "generic_button_edit": "Editer", + "generic_button_save": "Enregistrer", + "generic_button_cancel": "Annuler", + "generic_button_rss": "RSS", "LIVE": "EN DIRECT", "Shared `x` ago": "Ajoutée il y a `x`", "Unsubscribe": "Se désabonner", @@ -149,6 +154,7 @@ "Title": "Titre", "Playlist privacy": "Paramètres de confidentialité de la liste de lecture", "Editing playlist `x`": "Modifier la liste de lecture `x`", + "playlist_button_add_items": "Ajouter des vidéos", "Show more": "Afficher plus", "Show less": "Afficher moins", "Watch on YouTube": "Voir la vidéo sur Youtube", diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr index f05e1338..decdcb2f 100644 --- a/src/invidious/views/components/item.ecr +++ b/src/invidious/views/components/item.ecr @@ -71,6 +71,11 @@ <%- if !env.get("preferences").as(Preferences).thin_mode -%> + + <% if item_watched %> +
    +
    + <% end %>
    <%- end -%> @@ -109,11 +114,6 @@

    <%= recode_length_seconds(item.length_seconds) %>

    <%- end -%>
    - - <% if item_watched %> -
    -
    - <% end %>
    diff --git a/src/invidious/views/edit_playlist.ecr b/src/invidious/views/edit_playlist.ecr index d2981886..34157c67 100644 --- a/src/invidious/views/edit_playlist.ecr +++ b/src/invidious/views/edit_playlist.ecr @@ -6,35 +6,43 @@ <% end %>
    -
    -
    +
    + +
    + +
    +

    +
    +
    + +
    +
    <%= HTML.escape(playlist.author) %> | <%= translate_count(locale, "generic_videos_count", playlist.video_count) %> | - <%= translate(locale, "Updated `x` ago", recode_date(playlist.updated, locale)) %> | - "> - -
    -
    -

    -
    - -
    -
    -
    -

    +
    @@ -44,14 +52,6 @@ -<% if playlist.is_a?(InvidiousPlaylist) && playlist.author == user.try &.email %> -
    -

    - -

    -
    -<% end %> -

    diff --git a/src/invidious/views/playlist.ecr b/src/invidious/views/playlist.ecr index 08995a83..8d4d116d 100644 --- a/src/invidious/views/playlist.ecr +++ b/src/invidious/views/playlist.ecr @@ -7,8 +7,51 @@ <% end %>
    -
    +

    <%= title %>

    +
    + +
    + <%- if playlist.is_a?(InvidiousPlaylist) && playlist.author == user.try &.email -%> + + + + <%- else -%> +
    + <%- if IV::Database::Playlists.exists?(playlist.id) -%> + +  <%= translate(locale, "Subscribe") %> + + <%- else -%> + +  <%= translate(locale, "Unsubscribe") %> + + <%- end -%> +
    + <%- end -%> + + +
    +
    + +
    +
    <% if playlist.is_a? InvidiousPlaylist %> <% if playlist.author == user.try &.email %> @@ -54,37 +97,12 @@
    <% end %>
    -
    -

    -
    - <% if playlist.is_a?(InvidiousPlaylist) && playlist.author == user.try &.email %> -
    -
    - <% else %> - <% if Invidious::Database::Playlists.exists?(playlist.id) %> -
    - <% else %> -
    - <% end %> - <% end %> -
    -
    -

    -
    <%= playlist.description_html %>
    -<% if playlist.is_a?(InvidiousPlaylist) && playlist.author == user.try &.email %> -
    -

    - -

    -
    -<% end %> -

    From 43dcab225caca7034346a79da340e434cdb4d407 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 22 Apr 2023 19:45:45 +0200 Subject: [PATCH 291/598] HTML: merge MixVideo with other types in item.ecr --- src/invidious/views/components/item.ecr | 25 ++++--------------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr index decdcb2f..0fa9c807 100644 --- a/src/invidious/views/components/item.ecr +++ b/src/invidious/views/components/item.ecr @@ -34,26 +34,6 @@

    <%= HTML.escape(item.author) %><% if !item.is_a?(InvidiousPlaylist) && !item.author_verified.nil? && item.author_verified %> <% end %>

    - <% when MixVideo %> - - <% if !env.get("preferences").as(Preferences).thin_mode %> -
    - - <% if item.length_seconds != 0 %> -

    <%= recode_length_seconds(item.length_seconds) %>

    - <% end %> - - <% if item_watched %> -
    -
    - <% end %> -
    - <% end %> -

    <%= HTML.escape(item.title) %>

    -
    - -

    <%= HTML.escape(item.author) %>

    -
    <% when Category %> <% else %> <%- @@ -61,6 +41,9 @@ if item.is_a?(PlaylistVideo) link_url = "/watch?v=#{item.id}&list=#{item.plid}&index=#{item.index}" endpoint_params = "?v=#{item.id}&list=#{item.plid}" + elsif item.is_a?(MixVideo) + link_url = "/watch?v=#{item.id}&list=#{item.rdid}" + endpoint_params = "?v=#{item.id}&list=#{item.rdid}" else link_url = "/watch?v=#{item.id}" endpoint_params = "?v=#{item.id}" @@ -134,7 +117,7 @@
    <% if item.responds_to?(:premiere_timestamp) && item.premiere_timestamp.try &.> Time.utc %>

    <%= translate(locale, "Premieres in `x`", recode_date((item.premiere_timestamp.as(Time) - Time.utc).ago, locale)) %>

    - <% elsif Time.utc - item.published > 1.minute %> + <% elsif item.responds_to?(:published) && (Time.utc - item.published) > 1.minute %>

    <%= translate(locale, "Shared `x` ago", recode_date(item.published, locale)) %>

    <% end %>
    From 8718f2068859b12174cecf4af11c30bfe64103a6 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 22 Apr 2023 19:59:01 +0200 Subject: [PATCH 292/598] HTML: Fix thin mode/thumbnail on other items --- src/invidious/views/components/item.ecr | 71 +++++++++++++++++-------- src/invidious/views/feeds/history.ecr | 28 +++++----- 2 files changed, 61 insertions(+), 38 deletions(-) diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr index 0fa9c807..9b73f7ee 100644 --- a/src/invidious/views/components/item.ecr +++ b/src/invidious/views/components/item.ecr @@ -1,39 +1,64 @@ -<% item_watched = !item.is_a?(SearchChannel | SearchPlaylist | InvidiousPlaylist | Category) && env.get?("user").try &.as(User).watched.index(item.id) != nil %> +<%- + thin_mode = env.get("preferences").as(Preferences).thin_mode + item_watched = !item.is_a?(SearchChannel | SearchPlaylist | InvidiousPlaylist | Category) && env.get?("user").try &.as(User).watched.index(item.id) != nil + author_verified = item.responds_to?(:author_verified) && item.author_verified +-%>
    <% case item when %> <% when SearchChannel %> - - <% if !env.get("preferences").as(Preferences).thin_mode %> + <% if !thin_mode %> +
    " alt="" />
    - <% end %> -

    <%= HTML.escape(item.author) %><% if !item.author_verified.nil? && item.author_verified %> <% end %>

    -
    + + <% end %> + + +

    <%= translate_count(locale, "generic_subscribers_count", item.subscriber_count, NumberFormatting::Separator) %>

    <% if !item.auto_generated %>

    <%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %>

    <% end %>
    <%= item.description_html %>
    <% when SearchPlaylist, InvidiousPlaylist %> - <% if item.id.starts_with? "RD" %> - <% url = "/mix?list=#{item.id}&continuation=#{URI.parse(item.thumbnail || "/vi/-----------").request_target.split("/")[2]}" %> - <% else %> - <% url = "/playlist?list=#{item.id}" %> - <% end %> + <%- + if item.id.starts_with? "RD" + link_url = "/mix?list=#{item.id}&continuation=#{URI.parse(item.thumbnail || "/vi/-----------").request_target.split("/")[2]}" + else + link_url = "/playlist?list=#{item.id}" + end + -%> - - <% if !env.get("preferences").as(Preferences).thin_mode %> - + + + + <% when Category %> <% else %> <%- @@ -106,7 +131,7 @@
    diff --git a/src/invidious/views/feeds/history.ecr b/src/invidious/views/feeds/history.ecr index 5301a232..83ea7238 100644 --- a/src/invidious/views/feeds/history.ecr +++ b/src/invidious/views/feeds/history.ecr @@ -31,22 +31,20 @@ <% watched.each do |item| %>
    - - <% if !env.get("preferences").as(Preferences).thin_mode %> -
    - +
    + + + -
    -
    " method="post"> - "> - -
    -
    -
    -

    - <% end %> - +
    +
    " method="post"> + "> + +
    +
    +
    +

    <% end %> From cc30b00f8ca00572348c1ee266df907c69726c13 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 22 Apr 2023 20:25:26 +0200 Subject: [PATCH 293/598] CSS: fix light/dark themes for pure buttons --- assets/css/default.css | 74 +++++++++++++++++++++++++++++------------- 1 file changed, 51 insertions(+), 23 deletions(-) diff --git a/assets/css/default.css b/assets/css/default.css index 7a99a0db..f671c3bf 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -139,25 +139,6 @@ body a.pure-button-primary, margin: 0 .4em; } -.dark-theme .pure-button-secondary { - background-color: #0002; - color: #ddd; -} - -button.pure-button-primary:hover, -button.pure-button-primary:focus, -body a.pure-button-primary:hover, -body a.pure-button-primary:focus { - background-color: rgba(0, 182, 240, 1); - color: #fff; -} - -.pure-button-secondary:hover, -.pure-button-secondary:focus { - color: rgb(0, 182, 240); - border-color: rgb(0, 182, 240); -} - .pure-button-secondary.low-profile { padding: 5px 10px; margin: 0; @@ -219,6 +200,7 @@ div.thumbnail > .bottom-right-overlay { } .length, .top-left-overlay button { + color: #eee; background-color: rgba(35, 35, 35, 0.85) !important; } @@ -449,9 +431,18 @@ span > select { color: #075A9E !important; } -.light-theme a.pure-button-primary:hover, -.light-theme a.pure-button-primary:focus { +.light-theme .pure-button-primary:hover, +.light-theme .pure-button-primary:focus, +.light-theme .pure-button-secondary:hover, +.light-theme .pure-button-secondary:focus { color: #fff !important; + border-color: rgba(0, 182, 240, 0.75) !important; + background-color: rgba(0, 182, 240, 0.75) !important; +} + +.light-theme .pure-button-secondary:not(.low-profile) { + color: #335d7a; + background-color: #fff2; } .light-theme a { @@ -479,9 +470,18 @@ span > select { color: #075A9E !important; } - .no-theme a.pure-button-primary:hover, - .no-theme a.pure-button-primary:focus { + .no-theme .pure-button-primary:hover, + .no-theme .pure-button-primary:focus, + .no-theme .pure-button-secondary:hover, + .no-theme .pure-button-secondary:focus { color: #fff !important; + border-color: rgba(0, 182, 240, 0.75) !important; + background-color: rgba(0, 182, 240, 0.75) !important; + } + + .no-theme .pure-button-secondary:not(.low-profile) { + color: #335d7a; + background-color: #fff2; } .no-theme a { @@ -514,6 +514,20 @@ span > select { color: rgb(0, 182, 240); } +.dark-theme .pure-button-primary:hover, +.dark-theme .pure-button-primary:focus, +.dark-theme .pure-button-secondary:hover, +.dark-theme .pure-button-secondary:focus { + color: #fff !important; + border-color: rgb(0, 182, 240) !important; + background-color: rgba(0, 182, 240, 1) !important; +} + +.dark-theme .pure-button-secondary { + background-color: #0002; + color: #ddd; +} + .dark-theme a { color: #a0a0a0; text-decoration: none; @@ -554,6 +568,20 @@ body.dark-theme { color: rgb(0, 182, 240); } + .no-theme .pure-button-primary:hover, + .no-theme .pure-button-primary:focus, + .no-theme .pure-button-secondary:hover, + .no-theme .pure-button-secondary:focus { + color: #fff !important; + border-color: rgb(0, 182, 240) !important; + background-color: rgba(0, 182, 240, 1) !important; + } + + .no-theme .pure-button-secondary { + background-color: #0002; + color: #ddd; + } + .no-theme a { color: #a0a0a0; text-decoration: none; From 42fa6ad2a30038cd7cdc705f5da2bffdc9714349 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Mon, 24 Apr 2023 19:44:15 +0200 Subject: [PATCH 294/598] HTML/CSS: Fix buttons' responsiveness --- assets/css/default.css | 94 ++++++++++++++----- .../components/video-context-buttons.ecr | 4 +- src/invidious/views/playlist.ecr | 18 ++-- 3 files changed, 78 insertions(+), 38 deletions(-) diff --git a/assets/css/default.css b/assets/css/default.css index f671c3bf..21121f4d 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -1,3 +1,7 @@ +/* + * Common attributes + */ + html, body { font-family: BlinkMacSystemFont, -apple-system, "Segoe UI", Roboto, Oxygen, @@ -11,6 +15,16 @@ body { min-height: 100vh; } +.h-box { + padding-left: 1em; + padding-right: 1em; +} + +.v-box { + padding-top: 1em; + padding-bottom: 1em; +} + .deleted { background-color: rgb(255, 0, 0, 0.5); } @@ -20,6 +34,34 @@ body { margin-bottom: 20px; } +.title { + margin: 0.5em 0 1em 0; +} + +/* A flex container */ +.flexible { + display: flex; + align-items: center; +} + +.flex-left { + display: flex; + flex: 1 1 auto; + flex-flow: row wrap; + justify-content: flex-start; +} +.flex-right { + display: flex; + flex: 2 0 auto; + flex-flow: row nowrap; + justify-content: flex-end; +} + + +/* + * Channel page + */ + .channel-profile > * { font-size: 1.17em; font-weight: bold; @@ -90,16 +132,6 @@ body a.channel-owner { } } -.h-box { - padding-left: 1em; - padding-right: 1em; -} - -.v-box { - padding-top: 1em; - padding-bottom: 1em; -} - div { overflow-wrap: break-word; word-wrap: break-word; @@ -144,9 +176,15 @@ body a.pure-button-primary, margin: 0; } +/* Has to be combined with flex-left/right */ +.button-container { + flex-flow: wrap; + gap: 0.5em 0.75em; +} + /* - * Thumbnails + * Video thumbnails */ div.thumbnail { @@ -280,6 +318,11 @@ input[type="search"]::-webkit-search-cancel-button { margin-right: 1em; } + +/* + * Responsive rules + */ + @media only screen and (max-aspect-ratio: 16/9) { .player-dimensions.vjs-fluid { padding-top: 46.86% !important; @@ -298,20 +341,28 @@ input[type="search"]::-webkit-search-cancel-button { .navbar > div { display: flex; justify-content: center; - } - - .navbar > div:not(:last-child) { - margin-bottom: 1em; + margin-bottom: 25px; } .navbar > .searchbar > form { - width: 60%; + width: 75%; } h1 { font-size: 1.25em; margin: 0.42em 0; } + + /* Space out the subscribe & RSS buttons and align them to the left */ + .title.flexible { display: block; } + .title.flexible > .flex-right { margin: 0.75em 0; justify-content: flex-start; } + + /* Space out buttons to make them easier to tap */ + .user-field { font-size: 125%; } + .user-field > :not(:last-child) { margin-right: 1.75em; } + + .icon-buttons { font-size: 125%; } + .icon-buttons > :not(:last-child) { margin-right: 0.75em; } } @media screen and (max-width: 320px) { @@ -328,10 +379,6 @@ input[type="search"]::-webkit-search-cancel-button { .video-card-row { margin: 15px 0; } -.flexible { display: flex; } -.flex-left { flex: 1 1 100%; flex-wrap: wrap; } -.flex-right { flex: 1 0 auto; flex-wrap: nowrap; } - p.channel-name { margin: 0; } p.video-data { margin: 0; font-weight: bold; font-size: 80%; } @@ -659,12 +706,7 @@ label[for="music-desc-expansion"]:hover { } /* Bidi (bidirectional text) support */ -h1, -h2, -h3, -h4, -h5, -p, +h1, h2, h3, h4, h5, p, #descriptionWrapper, #description-box, #music-description-box { diff --git a/src/invidious/views/components/video-context-buttons.ecr b/src/invidious/views/components/video-context-buttons.ecr index ddb6c983..385ed6b3 100644 --- a/src/invidious/views/components/video-context-buttons.ecr +++ b/src/invidious/views/components/video-context-buttons.ecr @@ -1,4 +1,4 @@ -
    +
    " href="https://www.youtube.com/watch<%=endpoint_params%>"> @@ -6,7 +6,7 @@ " href="/watch<%=endpoint_params%>&listen=1"> - + <% if env.get("preferences").as(Preferences).automatic_instance_redirect%> " href="/redirect?referer=%2Fwatch<%=URI.encode_www_form(endpoint_params)%>"> diff --git a/src/invidious/views/playlist.ecr b/src/invidious/views/playlist.ecr index 8d4d116d..ee9ba87b 100644 --- a/src/invidious/views/playlist.ecr +++ b/src/invidious/views/playlist.ecr @@ -6,30 +6,28 @@ <% end %> -
    -
    -

    <%= title %>

    -
    +
    +

    <%= title %>

    -
    +
    <%- if playlist.is_a?(InvidiousPlaylist) && playlist.author == user.try &.email -%> -
    + -
    + -
    + <%- else -%> -
    +
    <%- if IV::Database::Playlists.exists?(playlist.id) -%>  <%= translate(locale, "Subscribe") %> @@ -42,7 +40,7 @@
    <%- end -%> -
    +
     <%= translate(locale, "generic_button_rss") %> From 411208bbd211d7effe278eabe23d5e2f502b5ea6 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Mon, 24 Apr 2023 20:28:40 +0200 Subject: [PATCH 295/598] HTML: Reorder buttons on the channel and watch pages --- .../views/components/channel_info.ecr | 29 ++++++++--------- .../views/components/subscribe_widget.ecr | 6 ---- src/invidious/views/watch.ecr | 31 ++++++++++++------- 3 files changed, 35 insertions(+), 31 deletions(-) diff --git a/src/invidious/views/components/channel_info.ecr b/src/invidious/views/components/channel_info.ecr index 59888760..f4164f31 100644 --- a/src/invidious/views/components/channel_info.ecr +++ b/src/invidious/views/components/channel_info.ecr @@ -8,29 +8,30 @@
    <% end %> -
    -
    +
    +
    <%= author %><% if !channel.verified.nil? && channel.verified %> <% end %>
    -
    -

    - -

    + +
    +
    + <% sub_count_text = number_to_short_text(channel.sub_count) %> + <%= rendered "components/subscribe_widget" %> +
    + +
    -
    -

    <%= channel.description_html %>

    -
    -
    - -
    - <% sub_count_text = number_to_short_text(channel.sub_count) %> - <%= rendered "components/subscribe_widget" %> +

    <%= channel.description_html %>

    diff --git a/src/invidious/views/components/subscribe_widget.ecr b/src/invidious/views/components/subscribe_widget.ecr index b9d5f783..05e4e253 100644 --- a/src/invidious/views/components/subscribe_widget.ecr +++ b/src/invidious/views/components/subscribe_widget.ecr @@ -1,22 +1,18 @@ <% if user %> <% if subscriptions.includes? ucid %> -

    " method="post"> ">
    -

    <% else %> -

    " method="post"> ">
    -

    <% end %> <% else %> -

    "> <%= translate(locale, "Subscribe") %> | <%= sub_count_text %> -

    <% end %> diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 5b3190f3..4f4354a9 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -204,19 +204,28 @@ we're going to need to do it here in order to allow for translations.
    -
    - -
    - <% if !video.author_thumbnail.empty? %> - - <% end %> - <%= author %><% if !video.author_verified.nil? && video.author_verified %> <% end %> + +
    + + +
    +
    + <% sub_count_text = video.sub_count_text %> + <%= rendered "components/subscribe_widget" %>
    - - - <% sub_count_text = video.sub_count_text %> - <%= rendered "components/subscribe_widget" %> +
    +
    +

    <% if video.premiere_timestamp.try &.> Time.utc %> <%= video.premiere_timestamp.try { |t| translate(locale, "Premieres `x`", t.to_s("%B %-d, %R UTC")) } %> From 06b2bab795ebf54e9c6a396e37a129a87d39675a Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Mon, 24 Apr 2023 22:19:46 +0200 Subject: [PATCH 296/598] HTML: Fix thumbnails of related videos (watch page) --- src/invidious/views/watch.ecr | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 4f4354a9..9275631c 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -304,15 +304,26 @@ we're going to need to do it here in order to allow for translations. <% video.related_videos.each do |rv| %> <% if rv["id"]? %> - &listen=<%= params.listen %>"> - <% if !env.get("preferences").as(Preferences).thin_mode %> -

    +
    + + - <% end %> -

    <%= rv["title"] %>

    -
    + + <%- end -%> + +
    + <%- if (length_seconds = rv["length_seconds"]?.try &.to_i?) && length_seconds != 0 -%> +

    <%= recode_length_seconds(length_seconds) %>

    + <%- end -%> +
    +
    + + +
    <% if rv["ucid"]? %> @@ -330,6 +341,8 @@ we're going to need to do it here in order to allow for translations. %>
    + +
    <% end %> <% end %>
    From c17404890ca9618ebc828a06bc88ff2bd79e811e Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Tue, 23 May 2023 22:49:44 +0200 Subject: [PATCH 297/598] HTML: Use the new pagination component for history/subscriptions --- src/invidious/routes/feeds.cr | 8 +++++++ src/invidious/views/feeds/history.ecr | 24 ++++++-------------- src/invidious/views/feeds/subscriptions.ecr | 25 +++++++-------------- 3 files changed, 23 insertions(+), 34 deletions(-) diff --git a/src/invidious/routes/feeds.cr b/src/invidious/routes/feeds.cr index fc62c5a3..a8246b2e 100644 --- a/src/invidious/routes/feeds.cr +++ b/src/invidious/routes/feeds.cr @@ -102,6 +102,10 @@ module Invidious::Routes::Feeds end env.set "user", user + # Used for pagination links + base_url = "/feed/subscriptions" + base_url += "?max_results=#{max_results}" if env.params.query.has_key?("max_results") + templated "feeds/subscriptions" end @@ -129,6 +133,10 @@ module Invidious::Routes::Feeds end watched ||= [] of String + # Used for pagination links + base_url = "/feed/history" + base_url += "?max_results=#{max_results}" if env.params.query.has_key?("max_results") + templated "feeds/history" end diff --git a/src/invidious/views/feeds/history.ecr b/src/invidious/views/feeds/history.ecr index 83ea7238..bda4e1f3 100644 --- a/src/invidious/views/feeds/history.ecr +++ b/src/invidious/views/feeds/history.ecr @@ -50,20 +50,10 @@ <% end %>
    - +<%= + IV::Frontend::Pagination.nav_numeric(locale, + base_url: base_url, + current_page: page, + show_next: (watched.size >= max_results) + ) +%> diff --git a/src/invidious/views/feeds/subscriptions.ecr b/src/invidious/views/feeds/subscriptions.ecr index 9c69c5b0..c36bd00f 100644 --- a/src/invidious/views/feeds/subscriptions.ecr +++ b/src/invidious/views/feeds/subscriptions.ecr @@ -56,6 +56,7 @@ +
    <% videos.each do |item| %> <%= rendered "components/item" %> @@ -64,20 +65,10 @@ -
    - -
    -
    - <% if (videos.size + notifications.size) == max_results %> - &max_results=<%= max_results %><% end %>"> - <%= translate(locale, "Next page") %> - - <% end %> -
    -
    +<%= + IV::Frontend::Pagination.nav_numeric(locale, + base_url: base_url, + current_page: page, + show_next: ((videos.size + notifications.size) == max_results) + ) +%> From 9b75f79fb553403d0af7b2f9a1212a1e93bcf85b Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 8 Jul 2023 21:17:44 +0200 Subject: [PATCH 298/598] HTML/CSS: Add thumbnail placeholder in thin mode This change is required to make the overlay buttons functional (add to and delete from playlist, mark as watched, etc.) --- assets/css/default.css | 5 +++++ src/invidious/views/components/item.ecr | 8 +++++++- src/invidious/views/watch.ecr | 2 ++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/assets/css/default.css b/assets/css/default.css index 21121f4d..c31b24e5 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -199,6 +199,11 @@ img.thumbnail { object-fit: cover; } +.thumbnail-placeholder { + min-height: 50px; + border: 2px dotted; +} + div.watched-overlay { z-index: 50; position: absolute; diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr index 9b73f7ee..7ffd2d93 100644 --- a/src/invidious/views/components/item.ecr +++ b/src/invidious/views/components/item.ecr @@ -14,6 +14,8 @@ " alt="" /> + <%- else -%> +
    <% end %>
    @@ -41,6 +43,8 @@ " alt="" /> + <%- else -%> +
    <%- end -%>
    @@ -76,7 +80,7 @@ -%>
    - <%- if !env.get("preferences").as(Preferences).thin_mode -%> + <%- if !thin_mode -%> @@ -85,6 +89,8 @@
    <% end %>
    + <%- else -%> +
    <%- end -%>
    diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 9275631c..498d57a1 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -311,6 +311,8 @@ we're going to need to do it here in order to allow for translations. &listen=<%= params.listen %>"> /mqdefault.jpg" alt="" /> + <%- else -%> +
    <%- end -%>
    From 0110f865c39fd0a1d416502422110430f92f4ef3 Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Sat, 8 Jul 2023 16:51:19 -0400 Subject: [PATCH 299/598] Playlist import no refresh --- src/invidious/user/imports.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/user/imports.cr b/src/invidious/user/imports.cr index 0a2fe1e2..86d0ce6e 100644 --- a/src/invidious/user/imports.cr +++ b/src/invidious/user/imports.cr @@ -133,7 +133,7 @@ struct Invidious::User next if !video_id begin - video = get_video(video_id) + video = get_video(video_id, false) rescue ex next end From f2fa3da9d2f8ffc1684997526ddd5b3357d88897 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Wed, 12 Jul 2023 11:06:34 -0700 Subject: [PATCH 300/598] Add support for releases and podcasts tabs --- locales/en-US.json | 2 + src/invidious/channels/playlists.cr | 18 +++++++ src/invidious/frontend/channel_page.cr | 2 + src/invidious/routes/api/v1/channels.cr | 62 ++++++++++++++++++++++++- src/invidious/routes/channels.cr | 44 +++++++++++++++++- src/invidious/routing.cr | 5 ++ src/invidious/views/channel.ecr | 2 + src/invidious/yt_backend/extractors.cr | 5 +- 8 files changed, 134 insertions(+), 6 deletions(-) diff --git a/locales/en-US.json b/locales/en-US.json index e13ba968..29dd7a40 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -474,6 +474,8 @@ "channel_tab_videos_label": "Videos", "channel_tab_shorts_label": "Shorts", "channel_tab_streams_label": "Livestreams", + "channel_tab_podcasts_label": "Podcasts", + "channel_tab_releases_label": "Releases", "channel_tab_playlists_label": "Playlists", "channel_tab_community_label": "Community", "channel_tab_channels_label": "Channels" diff --git a/src/invidious/channels/playlists.cr b/src/invidious/channels/playlists.cr index 8dc824b2..91029fe3 100644 --- a/src/invidious/channels/playlists.cr +++ b/src/invidious/channels/playlists.cr @@ -26,3 +26,21 @@ def fetch_channel_playlists(ucid, author, continuation, sort_by) return extract_items(initial_data, author, ucid) end + +def fetch_channel_podcasts(ucid, author, continuation) + if continuation + initial_data = YoutubeAPI.browse(continuation) + else + initial_data = YoutubeAPI.browse(ucid, params: "Eghwb2RjYXN0c_IGBQoDugEA") + end + return extract_items(initial_data, author, ucid) +end + +def fetch_channel_releases(ucid, author, continuation) + if continuation + initial_data = YoutubeAPI.browse(continuation) + else + initial_data = YoutubeAPI.browse(ucid, params: "EghyZWxlYXNlc_IGBQoDsgEA") + end + return extract_items(initial_data, author, ucid) +end diff --git a/src/invidious/frontend/channel_page.cr b/src/invidious/frontend/channel_page.cr index 53745dd5..fe7d6d6e 100644 --- a/src/invidious/frontend/channel_page.cr +++ b/src/invidious/frontend/channel_page.cr @@ -5,6 +5,8 @@ module Invidious::Frontend::ChannelPage Videos Shorts Streams + Podcasts + Releases Playlists Community Channels diff --git a/src/invidious/routes/api/v1/channels.cr b/src/invidious/routes/api/v1/channels.cr index bcb4db2c..adf05d30 100644 --- a/src/invidious/routes/api/v1/channels.cr +++ b/src/invidious/routes/api/v1/channels.cr @@ -245,7 +245,7 @@ module Invidious::Routes::API::V1::Channels channel = nil # Make the compiler happy get_channel() - items, continuation = fetch_channel_playlists(channel.ucid, channel.author, continuation, sort_by) + items, next_continuation = fetch_channel_playlists(channel.ucid, channel.author, continuation, sort_by) JSON.build do |json| json.object do @@ -257,7 +257,65 @@ module Invidious::Routes::API::V1::Channels end end - json.field "continuation", continuation + json.field "continuation", next_continuation if next_continuation + end + end + end + + def self.podcasts(env) + locale = env.get("preferences").as(Preferences).locale + + env.response.content_type = "application/json" + + ucid = env.params.url["ucid"] + continuation = env.params.query["continuation"]? + + # Use the macro defined above + channel = nil # Make the compiler happy + get_channel() + + items, next_continuation = fetch_channel_podcasts(channel.ucid, channel.author, continuation) + + JSON.build do |json| + json.object do + json.field "playlists" do + json.array do + items.each do |item| + item.to_json(locale, json) if item.is_a?(SearchPlaylist) + end + end + end + + json.field "continuation", next_continuation if next_continuation + end + end + end + + def self.releases(env) + locale = env.get("preferences").as(Preferences).locale + + env.response.content_type = "application/json" + + ucid = env.params.url["ucid"] + continuation = env.params.query["continuation"]? + + # Use the macro defined above + channel = nil # Make the compiler happy + get_channel() + + items, next_continuation = fetch_channel_releases(channel.ucid, channel.author, continuation) + + JSON.build do |json| + json.object do + json.field "playlists" do + json.array do + items.each do |item| + item.to_json(locale, json) if item.is_a?(SearchPlaylist) + end + end + end + + json.field "continuation", next_continuation if next_continuation end end end diff --git a/src/invidious/routes/channels.cr b/src/invidious/routes/channels.cr index 16621994..9892ae2a 100644 --- a/src/invidious/routes/channels.cr +++ b/src/invidious/routes/channels.cr @@ -27,7 +27,7 @@ module Invidious::Routes::Channels item.author end end - items = items.select(SearchPlaylist).map(&.as(SearchPlaylist)) + items = items.select(SearchPlaylist) items.each(&.author = "") else sort_options = {"newest", "oldest", "popular"} @@ -105,13 +105,53 @@ module Invidious::Routes::Channels channel.ucid, channel.author, continuation, (sort_by || "last") ) - items = items.select(SearchPlaylist).map(&.as(SearchPlaylist)) + items = items.select(SearchPlaylist) items.each(&.author = "") selected_tab = Frontend::ChannelPage::TabsAvailable::Playlists templated "channel" end + def self.podcasts(env) + data = self.fetch_basic_information(env) + return data if !data.is_a?(Tuple) + + locale, user, subscriptions, continuation, ucid, channel = data + + sort_by = "" + sort_options = [] of String + + items, next_continuation = fetch_channel_podcasts( + channel.ucid, channel.author, continuation + ) + + items = items.select(SearchPlaylist) + items.each(&.author = "") + + selected_tab = Frontend::ChannelPage::TabsAvailable::Podcasts + templated "channel" + end + + def self.releases(env) + data = self.fetch_basic_information(env) + return data if !data.is_a?(Tuple) + + locale, user, subscriptions, continuation, ucid, channel = data + + sort_by = "" + sort_options = [] of String + + items, next_continuation = fetch_channel_releases( + channel.ucid, channel.author, continuation + ) + + items = items.select(SearchPlaylist) + items.each(&.author = "") + + selected_tab = Frontend::ChannelPage::TabsAvailable::Releases + templated "channel" + end + def self.community(env) data = self.fetch_basic_information(env) if !data.is_a?(Tuple) diff --git a/src/invidious/routing.cr b/src/invidious/routing.cr index daaf4d88..9c43171c 100644 --- a/src/invidious/routing.cr +++ b/src/invidious/routing.cr @@ -118,6 +118,8 @@ module Invidious::Routing get "/channel/:ucid/videos", Routes::Channels, :videos get "/channel/:ucid/shorts", Routes::Channels, :shorts get "/channel/:ucid/streams", Routes::Channels, :streams + get "/channel/:ucid/podcasts", Routes::Channels, :podcasts + get "/channel/:ucid/releases", Routes::Channels, :releases get "/channel/:ucid/playlists", Routes::Channels, :playlists get "/channel/:ucid/community", Routes::Channels, :community get "/channel/:ucid/channels", Routes::Channels, :channels @@ -228,6 +230,9 @@ module Invidious::Routing get "/api/v1/channels/:ucid", {{namespace}}::Channels, :home get "/api/v1/channels/:ucid/shorts", {{namespace}}::Channels, :shorts get "/api/v1/channels/:ucid/streams", {{namespace}}::Channels, :streams + get "/api/v1/channels/:ucid/podcasts", {{namespace}}::Channels, :podcasts + get "/api/v1/channels/:ucid/releases", {{namespace}}::Channels, :releases + get "/api/v1/channels/:ucid/channels", {{namespace}}::Channels, :channels {% for route in {"videos", "latest", "playlists", "community", "search"} %} diff --git a/src/invidious/views/channel.ecr b/src/invidious/views/channel.ecr index 6e62a471..066e25b5 100644 --- a/src/invidious/views/channel.ecr +++ b/src/invidious/views/channel.ecr @@ -9,6 +9,8 @@ when .streams? then "/channel/#{ucid}/streams" when .playlists? then "/channel/#{ucid}/playlists" when .channels? then "/channel/#{ucid}/channels" + when .podcasts? then "/channel/#{ucid}/podcasts" + when .releases? then "/channel/#{ucid}/releases" else "/channel/#{ucid}" end diff --git a/src/invidious/yt_backend/extractors.cr b/src/invidious/yt_backend/extractors.cr index 6686e6e7..e5029dc5 100644 --- a/src/invidious/yt_backend/extractors.cr +++ b/src/invidious/yt_backend/extractors.cr @@ -408,8 +408,8 @@ private module Parsers # Returns nil when the given object isn't a RichItemRenderer # # A richItemRenderer seems to be a simple wrapper for a videoRenderer, used - # by the result page for hashtags. It is located inside a continuationItems - # container. + # by the result page for hashtags and for the podcast tab on channels. + # It is located inside a continuationItems container for hashtags. # module RichItemRendererParser def self.process(item : JSON::Any, author_fallback : AuthorFallback) @@ -421,6 +421,7 @@ private module Parsers private def self.parse(item_contents, author_fallback) child = VideoRendererParser.process(item_contents, author_fallback) child ||= ReelItemRendererParser.process(item_contents, author_fallback) + child ||= PlaylistRendererParser.process(item_contents, author_fallback) return child end From 05cc5033910cabe7008832e8917b93ee3112a540 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 15 Jul 2023 12:57:26 +0000 Subject: [PATCH 301/598] Fix lint --- src/invidious/views/channel.ecr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/invidious/views/channel.ecr b/src/invidious/views/channel.ecr index 066e25b5..4b50e7a0 100644 --- a/src/invidious/views/channel.ecr +++ b/src/invidious/views/channel.ecr @@ -9,8 +9,8 @@ when .streams? then "/channel/#{ucid}/streams" when .playlists? then "/channel/#{ucid}/playlists" when .channels? then "/channel/#{ucid}/channels" - when .podcasts? then "/channel/#{ucid}/podcasts" - when .releases? then "/channel/#{ucid}/releases" + when .podcasts? then "/channel/#{ucid}/podcasts" + when .releases? then "/channel/#{ucid}/releases" else "/channel/#{ucid}" end From 70145cba31fb7fa14dafa3493c9133c01f642116 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 11 Jul 2023 20:49:36 -0700 Subject: [PATCH 302/598] Community: Parse `Quiz` attachments --- src/invidious/channels/community.cr | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/invidious/channels/community.cr b/src/invidious/channels/community.cr index aac4bc8a..671f6dee 100644 --- a/src/invidious/channels/community.cr +++ b/src/invidious/channels/community.cr @@ -216,6 +216,22 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode) parse_item(attachment) .as(SearchPlaylist) .to_json(locale, json) + when .has_key?("quizRenderer") + json.object do + attachment = attachment["quizRenderer"] + json.field "type", "quiz" + json.field "totalVotes", short_text_to_number(attachment["totalVotes"]["simpleText"].as_s.split(" ")[0]) + json.field "choices" do + json.array do + attachment["choices"].as_a.each do |choice| + json.object do + json.field "text", choice.dig("text", "runs", 0, "text").as_s + json.field "isCorrect", choice["isCorrect"].as_bool + end + end + end + end + end else json.object do json.field "type", "unknown" From c8ecfaabe156e41999cf3a130a28a67a62b37ccb Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sun, 16 Jul 2023 17:28:37 +0200 Subject: [PATCH 303/598] Assets: Add SVG image for hashtag results --- assets/hashtag.svg | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 assets/hashtag.svg diff --git a/assets/hashtag.svg b/assets/hashtag.svg new file mode 100644 index 00000000..55109825 --- /dev/null +++ b/assets/hashtag.svg @@ -0,0 +1,9 @@ + + + + + + + + + From 839e90aeff93a18d59cb4fc53eb25cc5c152b44a Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 15 Jul 2023 15:41:04 +0200 Subject: [PATCH 304/598] Extractors: Add module for 'hashtagTileRenderer' --- src/invidious/helpers/serialized_yt_data.cr | 21 +++++++- src/invidious/yt_backend/extractors.cr | 53 ++++++++++++++++++++- 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/src/invidious/helpers/serialized_yt_data.cr b/src/invidious/helpers/serialized_yt_data.cr index 7c12ad0e..e0bd7279 100644 --- a/src/invidious/helpers/serialized_yt_data.cr +++ b/src/invidious/helpers/serialized_yt_data.cr @@ -232,6 +232,25 @@ struct SearchChannel end end +struct SearchHashtag + include DB::Serializable + + property title : String + property url : String + property video_count : Int64 + property channel_count : Int64 + + def to_json(locale : String?, json : JSON::Builder) + json.object do + json.field "type", "hashtag" + json.field "title", self.title + json.field "url", self.url + json.field "videoCount", self.video_count + json.field "channelCount", self.channel_count + end + end +end + class Category include DB::Serializable @@ -274,4 +293,4 @@ struct Continuation end end -alias SearchItem = SearchVideo | SearchChannel | SearchPlaylist | Category +alias SearchItem = SearchVideo | SearchChannel | SearchPlaylist | SearchHashtag | Category diff --git a/src/invidious/yt_backend/extractors.cr b/src/invidious/yt_backend/extractors.cr index e5029dc5..8456313b 100644 --- a/src/invidious/yt_backend/extractors.cr +++ b/src/invidious/yt_backend/extractors.cr @@ -11,15 +11,16 @@ private ITEM_CONTAINER_EXTRACTOR = { } private ITEM_PARSERS = { + Parsers::RichItemRendererParser, Parsers::VideoRendererParser, Parsers::ChannelRendererParser, Parsers::GridPlaylistRendererParser, Parsers::PlaylistRendererParser, Parsers::CategoryRendererParser, - Parsers::RichItemRendererParser, Parsers::ReelItemRendererParser, Parsers::ItemSectionRendererParser, Parsers::ContinuationItemRendererParser, + Parsers::HashtagRendererParser, } private alias InitialData = Hash(String, JSON::Any) @@ -210,6 +211,56 @@ private module Parsers end end + # Parses an Innertube `hashtagTileRenderer` into a `SearchHashtag`. + # Returns `nil` when the given object is not a `hashtagTileRenderer`. + # + # A `hashtagTileRenderer` is a kind of search result. + # It can be found when searching for any hashtag (e.g "#hi" or "#shorts") + module HashtagRendererParser + def self.process(item : JSON::Any, author_fallback : AuthorFallback) + if item_contents = item["hashtagTileRenderer"]? + return self.parse(item_contents) + end + end + + private def self.parse(item_contents) + title = extract_text(item_contents["hashtag"]).not_nil! # E.g "#hi" + + # E.g "/hashtag/hi" + url = item_contents.dig?("onTapCommand", "commandMetadata", "webCommandMetadata", "url").try &.as_s + url ||= URI.encode_path("/hashtag/#{title.lchop('#')}") + + video_count_txt = extract_text(item_contents["hashtagVideoCount"]?) # E.g "203K videos" + channel_count_txt = extract_text(item_contents["hashtagChannelCount"]?) # E.g "81K channels" + + # Fallback for video/channel counts + if channel_count_txt.nil? || video_count_txt.nil? + # E.g: "203K videos • 81K channels" + info_text = extract_text(item_contents["hashtagInfoText"]?).try &.split(" • ") + + if info_text && info_text.size == 2 + video_count_txt ||= info_text[0] + channel_count_txt ||= info_text[1] + end + end + + return SearchHashtag.new({ + title: title, + url: url, + video_count: short_text_to_number(video_count_txt || ""), + channel_count: short_text_to_number(channel_count_txt || ""), + }) + rescue ex + LOGGER.debug("HashtagRendererParser: Failed to extract renderer.") + LOGGER.debug("HashtagRendererParser: Got exception: #{ex.message}") + return nil + end + + def self.parser_name + return {{@type.name}} + end + end + # Parses a InnerTube gridPlaylistRenderer into a SearchPlaylist. Returns nil when the given object isn't a gridPlaylistRenderer # # A gridPlaylistRenderer renders a playlist, that is located in a grid, to click on within the YouTube and Invidious UI. From f38d1f33b140a1de13e20d14b7a1ff0fcf0a40b4 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 15 Jul 2023 15:42:46 +0200 Subject: [PATCH 305/598] HTML: Add UI element for 'SearchHashtag' in item.ecr --- src/invidious/views/components/item.ecr | 26 ++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr index 7ffd2d93..c29ec47b 100644 --- a/src/invidious/views/components/item.ecr +++ b/src/invidious/views/components/item.ecr @@ -1,6 +1,6 @@ <%- thin_mode = env.get("preferences").as(Preferences).thin_mode - item_watched = !item.is_a?(SearchChannel | SearchPlaylist | InvidiousPlaylist | Category) && env.get?("user").try &.as(User).watched.index(item.id) != nil + item_watched = !item.is_a?(SearchChannel | SearchHashtag | SearchPlaylist | InvidiousPlaylist | Category) && env.get?("user").try &.as(User).watched.index(item.id) != nil author_verified = item.responds_to?(:author_verified) && item.author_verified -%> @@ -29,6 +29,30 @@

    <%= translate_count(locale, "generic_subscribers_count", item.subscriber_count, NumberFormatting::Separator) %>

    <% if !item.auto_generated %>

    <%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %>

    <% end %>
    <%= item.description_html %>
    + <% when SearchHashtag %> + <% if !thin_mode %> + +
    +
    + <%- else -%> +
    + <% end %> + + + +
    + <%- if item.video_count != 0 -%> +

    <%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %>

    + <%- end -%> +
    + +
    + <%- if item.channel_count != 0 -%> +

    <%= translate_count(locale, "generic_channels_count", item.channel_count, NumberFormatting::Separator) %>

    + <%- end -%> +
    <% when SearchPlaylist, InvidiousPlaylist %> <%- if item.id.starts_with? "RD" From c1a69e4a4a8b581ec743b7b3f741097d6596cb3b Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sun, 16 Jul 2023 17:23:23 +0200 Subject: [PATCH 306/598] Channels: Use innertube to fetch the community tab --- src/invidious/channels/community.cr | 54 +++++++++----------------- src/invidious/yt_backend/extractors.cr | 26 ++++++++----- 2 files changed, 34 insertions(+), 46 deletions(-) diff --git a/src/invidious/channels/community.cr b/src/invidious/channels/community.cr index aac4bc8a..1a54a946 100644 --- a/src/invidious/channels/community.cr +++ b/src/invidious/channels/community.cr @@ -1,49 +1,31 @@ private IMAGE_QUALITIES = {320, 560, 640, 1280, 2000} # TODO: Add "sort_by" -def fetch_channel_community(ucid, continuation, locale, format, thin_mode) - response = YT_POOL.client &.get("/channel/#{ucid}/community?gl=US&hl=en") - if response.status_code != 200 - response = YT_POOL.client &.get("/user/#{ucid}/community?gl=US&hl=en") - end +def fetch_channel_community(ucid, cursor, locale, format, thin_mode) + if cursor.nil? + # Egljb21tdW5pdHk%3D is the protobuf object to load "community" + initial_data = YoutubeAPI.browse(ucid, params: "Egljb21tdW5pdHk%3D") - if response.status_code != 200 - raise NotFoundException.new("This channel does not exist.") - end - - ucid = response.body.match(/https:\/\/www.youtube.com\/channel\/(?UC[a-zA-Z0-9_-]{22})/).not_nil!["ucid"] - - if !continuation || continuation.empty? - initial_data = extract_initial_data(response.body) - body = extract_selected_tab(initial_data["contents"]["twoColumnBrowseResultsRenderer"]["tabs"])["content"]["sectionListRenderer"]["contents"][0]["itemSectionRenderer"] - - if !body - raise InfoException.new("Could not extract community tab.") + items = [] of JSON::Any + extract_items(initial_data) do |item| + items << item end else - continuation = produce_channel_community_continuation(ucid, continuation) + continuation = produce_channel_community_continuation(ucid, cursor) + initial_data = YoutubeAPI.browse(continuation: continuation) - headers = HTTP::Headers.new - headers["cookie"] = response.cookies.add_request_headers(headers)["cookie"] + container = initial_data.dig?("continuationContents", "itemSectionContinuation", "contents") - session_token = response.body.match(/"XSRF_TOKEN":"(?[^"]+)"/).try &.["session_token"]? || "" - post_req = { - session_token: session_token, - } + raise InfoException.new("Can't extract community data") if container.nil? - body = YoutubeAPI.browse(continuation) - - body = body.dig?("continuationContents", "itemSectionContinuation") || - body.dig?("continuationContents", "backstageCommentsContinuation") - - if !body - raise InfoException.new("Could not extract continuation.") - end + items = container.as_a end - posts = body["contents"].as_a + return extract_channel_community(items, ucid: ucid, locale: locale, format: format, thin_mode: thin_mode) +end - if message = posts[0]["messageRenderer"]? +def extract_channel_community(items, *, ucid, locale, format, thin_mode) + if message = items[0]["messageRenderer"]? error_message = (message["text"]["simpleText"]? || message["text"]["runs"]?.try &.[0]?.try &.["text"]?) .try &.as_s || "" @@ -59,7 +41,7 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode) json.field "authorId", ucid json.field "comments" do json.array do - posts.each do |post| + items.each do |post| comments = post["backstagePostThreadRenderer"]?.try &.["comments"]? || post["backstageCommentsContinuation"]? @@ -242,7 +224,7 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode) end end end - if cont = posts.dig?(-1, "continuationItemRenderer", "continuationEndpoint", "continuationCommand", "token") + if cont = items.dig?(-1, "continuationItemRenderer", "continuationEndpoint", "continuationCommand", "token") json.field "continuation", extract_channel_community_cursor(cont.as_s) end end diff --git a/src/invidious/yt_backend/extractors.cr b/src/invidious/yt_backend/extractors.cr index e5029dc5..8cf59d50 100644 --- a/src/invidious/yt_backend/extractors.cr +++ b/src/invidious/yt_backend/extractors.cr @@ -608,19 +608,25 @@ private module Extractors private def self.unpack_section_list(contents) raw_items = [] of JSON::Any - contents.as_a.each do |renderer_container| - renderer_container_contents = renderer_container["itemSectionRenderer"]["contents"][0] - - # Category extraction - if items_container = renderer_container_contents["shelfRenderer"]? - raw_items << renderer_container_contents - next - elsif items_container = renderer_container_contents["gridRenderer"]? + contents.as_a.each do |item| + if item_section_content = item.dig?("itemSectionRenderer", "contents") + raw_items += self.unpack_item_section(item_section_content) else - items_container = renderer_container_contents + raw_items << item end + end - items_container["items"]?.try &.as_a.each do |item| + return raw_items + end + + private def self.unpack_item_section(contents) + raw_items = [] of JSON::Any + + contents.as_a.each do |item| + # Category extraction + if container = item.dig?("gridRenderer", "items") || item.dig?("items") + raw_items += container.as_a + else raw_items << item end end From 2e67b90540d35ede212866e1fb597fd57ced35d5 Mon Sep 17 00:00:00 2001 From: syeopite Date: Sat, 22 Jul 2023 23:55:05 -0700 Subject: [PATCH 307/598] Add method to query /youtubei/v1/get_transcript --- src/invidious/yt_backend/youtube_api.cr | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/invidious/yt_backend/youtube_api.cr b/src/invidious/yt_backend/youtube_api.cr index 3dd9e9d8..f8aca04d 100644 --- a/src/invidious/yt_backend/youtube_api.cr +++ b/src/invidious/yt_backend/youtube_api.cr @@ -557,6 +557,30 @@ module YoutubeAPI return self._post_json("/youtubei/v1/search", data, client_config) end + #################################################################### + # transcript(params) + # + # Requests the youtubei/v1/get_transcript endpoint with the required headers + # and POST data in order to get a JSON reply. + # + # The requested data is a specially encoded protobuf string that denotes the specific language requested. + # + # An optional ClientConfig parameter can be passed, too (see + # `struct ClientConfig` above for more details). + # + + def transcript( + params : String, + client_config : ClientConfig | Nil = nil + ) : Hash(String, JSON::Any) + data = { + "context" => self.make_context(client_config), + "params" => params, + } + + return self._post_json("/youtubei/v1/get_transcript", data, client_config) + end + #################################################################### # _post_json(endpoint, data, client_config?) # From 7e5935a9da5355bbdd4c047edf692b0ce57722c7 Mon Sep 17 00:00:00 2001 From: syeopite Date: Sun, 23 Jul 2023 00:54:43 -0700 Subject: [PATCH 308/598] Rename Caption struct to CaptionMetadata The Caption object does not actually store any text lines for the subtitles. Instead it stores the metadata needed to display and fetch the actual captions from the YT timedtext API. Therefore it may be wiser to rename the struct to be more reflective of its current usage as well as the future usage once the current caption retrival system is replaced via InnerTube's transcript API --- src/invidious/frontend/watch_page.cr | 2 +- src/invidious/videos.cr | 6 +++--- src/invidious/videos/caption.cr | 8 ++++---- src/invidious/views/user/preferences.ecr | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/invidious/frontend/watch_page.cr b/src/invidious/frontend/watch_page.cr index e3214469..b860dba7 100644 --- a/src/invidious/frontend/watch_page.cr +++ b/src/invidious/frontend/watch_page.cr @@ -7,7 +7,7 @@ module Invidious::Frontend::WatchPage getter full_videos : Array(Hash(String, JSON::Any)) getter video_streams : Array(Hash(String, JSON::Any)) getter audio_streams : Array(Hash(String, JSON::Any)) - getter captions : Array(Invidious::Videos::Caption) + getter captions : Array(Invidious::Videos::CaptionMetadata) def initialize( @full_videos, diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index f38b33e5..2b1d2603 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -24,7 +24,7 @@ struct Video property updated : Time @[DB::Field(ignore: true)] - @captions = [] of Invidious::Videos::Caption + @captions = [] of Invidious::Videos::CaptionMetadata @[DB::Field(ignore: true)] property adaptive_fmts : Array(Hash(String, JSON::Any))? @@ -215,9 +215,9 @@ struct Video keywords.includes? "YouTube Red" end - def captions : Array(Invidious::Videos::Caption) + def captions : Array(Invidious::Videos::CaptionMetadata) if @captions.empty? && @info.has_key?("captions") - @captions = Invidious::Videos::Caption.from_yt_json(info["captions"]) + @captions = Invidious::Videos::CaptionMetadata.from_yt_json(info["captions"]) end return @captions diff --git a/src/invidious/videos/caption.cr b/src/invidious/videos/caption.cr index 13f81a31..c85b46c3 100644 --- a/src/invidious/videos/caption.cr +++ b/src/invidious/videos/caption.cr @@ -1,7 +1,7 @@ require "json" module Invidious::Videos - struct Caption + struct CaptionMetadata property name : String property language_code : String property base_url : String @@ -10,12 +10,12 @@ module Invidious::Videos end # Parse the JSON structure from Youtube - def self.from_yt_json(container : JSON::Any) : Array(Caption) + def self.from_yt_json(container : JSON::Any) : Array(CaptionMetadata) caption_tracks = container .dig?("playerCaptionsTracklistRenderer", "captionTracks") .try &.as_a - captions_list = [] of Caption + captions_list = [] of CaptionMetadata return captions_list if caption_tracks.nil? caption_tracks.each do |caption| @@ -25,7 +25,7 @@ module Invidious::Videos language_code = caption["languageCode"].to_s base_url = caption["baseUrl"].to_s - captions_list << Caption.new(name, language_code, base_url) + captions_list << CaptionMetadata.new(name, language_code, base_url) end return captions_list diff --git a/src/invidious/views/user/preferences.ecr b/src/invidious/views/user/preferences.ecr index dfda1434..b1061ee8 100644 --- a/src/invidious/views/user/preferences.ecr +++ b/src/invidious/views/user/preferences.ecr @@ -89,7 +89,7 @@ <% preferences.captions.each_with_index do |caption, index| %> From 8e18d445a7adf9a0c0887249003a7b84f0fb95af Mon Sep 17 00:00:00 2001 From: syeopite Date: Sun, 23 Jul 2023 01:52:53 -0700 Subject: [PATCH 309/598] Add method to generate params for transcripts api --- src/invidious/videos/transcript.cr | 34 ++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/invidious/videos/transcript.cr diff --git a/src/invidious/videos/transcript.cr b/src/invidious/videos/transcript.cr new file mode 100644 index 00000000..c50f7569 --- /dev/null +++ b/src/invidious/videos/transcript.cr @@ -0,0 +1,34 @@ +module Invidious::Videos + # Namespace for methods primarily relating to Transcripts + module Transcript + def self.generate_param(video_id : String, language_code : String, auto_generated : Bool) : String + if !auto_generated + is_auto_generated = "" + elsif is_auto_generated = "asr" + end + + object = { + "1:0:string" => video_id, + + "2:base64" => { + "1:string" => is_auto_generated, + "2:string" => language_code, + "3:string" => "", + }, + + "3:varint" => 1_i64, + "5:string" => "engagement-panel-searchable-transcript-search-panel", + "6:varint" => 1_i64, + "7:varint" => 1_i64, + "8:varint" => 1_i64, + } + + params = 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 params + end + end +end From 4b3ac1a757a5ee14919e83a84de31a3d0bd14a4c Mon Sep 17 00:00:00 2001 From: syeopite Date: Sun, 23 Jul 2023 03:22:19 -0700 Subject: [PATCH 310/598] Add method to parse transcript JSON into structs --- src/invidious/videos/transcript.cr | 37 ++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/invidious/videos/transcript.cr b/src/invidious/videos/transcript.cr index c50f7569..0d8b0b25 100644 --- a/src/invidious/videos/transcript.cr +++ b/src/invidious/videos/transcript.cr @@ -1,6 +1,8 @@ module Invidious::Videos # Namespace for methods primarily relating to Transcripts module Transcript + record TranscriptLine, start_ms : Time::Span, end_ms : Time::Span, line : String + def self.generate_param(video_id : String, language_code : String, auto_generated : Bool) : String if !auto_generated is_auto_generated = "" @@ -30,5 +32,40 @@ module Invidious::Videos return params end + + def self.convert_transcripts_to_vtt(initial_data : JSON::Any, target_language : String) : String + # Convert into TranscriptLine + + vtt = String.build do |vtt| + result << <<-END_VTT + WEBVTT + Kind: captions + Language: #{tlang} + + + END_VTT + + vtt << "\n\n" + end + end + + def self.parse(initial_data : Hash(String, JSON::Any)) + body = initial_data.dig("actions", 0, "updateEngagementPanelAction", "content", "transcriptRenderer", + "content", "transcriptSearchPanelRenderer", "body", "transcriptSegmentListRenderer", + "initialSegments").as_a + + lines = [] of TranscriptLine + body.each do |line| + line = line["transcriptSegmentRenderer"] + start_ms = line["startMs"].as_s.to_i.millisecond + end_ms = line["endMs"].as_s.to_i.millisecond + + text = extract_text(line["snippet"]) || "" + + lines << TranscriptLine.new(start_ms, end_ms, text) + end + + return lines + end end end From caac7e21668dd88eaf3d57ddc300427885af0a23 Mon Sep 17 00:00:00 2001 From: syeopite Date: Sun, 23 Jul 2023 03:52:26 -0700 Subject: [PATCH 311/598] Add method to convert transcripts response to vtt --- src/invidious/videos/transcript.cr | 39 ++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/src/invidious/videos/transcript.cr b/src/invidious/videos/transcript.cr index 0d8b0b25..ec990883 100644 --- a/src/invidious/videos/transcript.cr +++ b/src/invidious/videos/transcript.cr @@ -33,23 +33,52 @@ module Invidious::Videos return params end - def self.convert_transcripts_to_vtt(initial_data : JSON::Any, target_language : String) : String - # Convert into TranscriptLine + def self.convert_transcripts_to_vtt(initial_data : Hash(String, JSON::Any), target_language : String) : String + # Convert into array of TranscriptLine + lines = self.parse(initial_data) + # Taken from Invidious::Videos::CaptionMetadata.timedtext_to_vtt() vtt = String.build do |vtt| - result << <<-END_VTT + vtt << <<-END_VTT WEBVTT Kind: captions - Language: #{tlang} + Language: #{target_language} END_VTT vtt << "\n\n" + + lines.each do |line| + start_time = line.start_ms + end_time = line.end_ms + + # start_time + vtt << start_time.hours.to_s.rjust(2, '0') + vtt << ':' << start_time.minutes.to_s.rjust(2, '0') + vtt << ':' << start_time.seconds.to_s.rjust(2, '0') + vtt << '.' << start_time.milliseconds.to_s.rjust(3, '0') + + vtt << " --> " + + # end_time + vtt << end_time.hours.to_s.rjust(2, '0') + vtt << ':' << end_time.minutes.to_s.rjust(2, '0') + vtt << ':' << end_time.seconds.to_s.rjust(2, '0') + vtt << '.' << end_time.milliseconds.to_s.rjust(3, '0') + + vtt << "\n" + vtt << line.line + + vtt << "\n" + vtt << "\n" + end end + + return vtt end - def self.parse(initial_data : Hash(String, JSON::Any)) + private def self.parse(initial_data : Hash(String, JSON::Any)) body = initial_data.dig("actions", 0, "updateEngagementPanelAction", "content", "transcriptRenderer", "content", "transcriptSearchPanelRenderer", "body", "transcriptSegmentListRenderer", "initialSegments").as_a From e4942b188f5c192d5693687698db9b106571332c Mon Sep 17 00:00:00 2001 From: syeopite Date: Sun, 23 Jul 2023 05:02:02 -0700 Subject: [PATCH 312/598] Integrate transcript captions into captions API --- config/config.example.yml | 13 +++ src/invidious/config.cr | 3 + src/invidious/routes/api/v1/videos.cr | 112 ++++++++++++++------------ src/invidious/videos/caption.cr | 11 ++- src/invidious/videos/transcript.cr | 6 ++ 5 files changed, 91 insertions(+), 54 deletions(-) diff --git a/config/config.example.yml b/config/config.example.yml index 34070fe5..51beab89 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -182,6 +182,19 @@ https_only: false #force_resolve: +## +## Use Innertube's transcripts API instead of timedtext for closed captions +## +## Useful for larger instances as InnerTube is **not ratelimited**. See https://github.com/iv-org/invidious/issues/2567 +## +## Subtitle experience may differ slightly on Invidious. +## +## Accepted values: true, false +## Default: false +## +# use_innertube_for_captions: false + + # ----------------------------- # Logging # ----------------------------- diff --git a/src/invidious/config.cr b/src/invidious/config.cr index e5f1e822..c88a4837 100644 --- a/src/invidious/config.cr +++ b/src/invidious/config.cr @@ -129,6 +129,9 @@ class Config # Use quic transport for youtube api property use_quic : Bool = false + # Use Innertube's transcripts API instead of timedtext for closed captions + property use_innertube_for_captions : Bool = false + # Saved cookies in "name1=value1; name2=value2..." format @[YAML::Field(converter: Preferences::StringToCookies)] property cookies : HTTP::Cookies = HTTP::Cookies.new diff --git a/src/invidious/routes/api/v1/videos.cr b/src/invidious/routes/api/v1/videos.cr index af4fc806..000e64b9 100644 --- a/src/invidious/routes/api/v1/videos.cr +++ b/src/invidious/routes/api/v1/videos.cr @@ -87,70 +87,78 @@ module Invidious::Routes::API::V1::Videos caption = caption[0] end - url = URI.parse("#{caption.base_url}&tlang=#{tlang}").request_target + if CONFIG.use_innertube_for_captions + params = Invidious::Videos::Transcript.generate_param(id, caption.language_code, caption.auto_generated) + initial_data = YoutubeAPI.transcript(params.to_s) - # Auto-generated captions often have cues that aren't aligned properly with the video, - # as well as some other markup that makes it cumbersome, so we try to fix that here - if caption.name.includes? "auto-generated" - caption_xml = YT_POOL.client &.get(url).body + webvtt = Invidious::Videos::Transcript.convert_transcripts_to_vtt(initial_data, caption.language_code) + else + # Timedtext API handling + url = URI.parse("#{caption.base_url}&tlang=#{tlang}").request_target - if caption_xml.starts_with?(" i + 1 - end_time = caption_nodes[i + 1]["start"].to_f.seconds - else - end_time = start_time + duration + if caption_nodes.size > i + 1 + end_time = caption_nodes[i + 1]["start"].to_f.seconds + else + end_time = start_time + duration + end + + start_time = "#{start_time.hours.to_s.rjust(2, '0')}:#{start_time.minutes.to_s.rjust(2, '0')}:#{start_time.seconds.to_s.rjust(2, '0')}.#{start_time.milliseconds.to_s.rjust(3, '0')}" + end_time = "#{end_time.hours.to_s.rjust(2, '0')}:#{end_time.minutes.to_s.rjust(2, '0')}:#{end_time.seconds.to_s.rjust(2, '0')}.#{end_time.milliseconds.to_s.rjust(3, '0')}" + + text = HTML.unescape(node.content) + text = text.gsub(//, "") + text = text.gsub(/<\/font>/, "") + if md = text.match(/(?.*) : (?.*)/) + text = "#{md["text"]}" + end + + str << <<-END_CUE + #{start_time} --> #{end_time} + #{text} + + + END_CUE end - - start_time = "#{start_time.hours.to_s.rjust(2, '0')}:#{start_time.minutes.to_s.rjust(2, '0')}:#{start_time.seconds.to_s.rjust(2, '0')}.#{start_time.milliseconds.to_s.rjust(3, '0')}" - end_time = "#{end_time.hours.to_s.rjust(2, '0')}:#{end_time.minutes.to_s.rjust(2, '0')}:#{end_time.seconds.to_s.rjust(2, '0')}.#{end_time.milliseconds.to_s.rjust(3, '0')}" - - text = HTML.unescape(node.content) - text = text.gsub(//, "") - text = text.gsub(/<\/font>/, "") - if md = text.match(/(?.*) : (?.*)/) - text = "#{md["text"]}" - end - - str << <<-END_CUE - #{start_time} --> #{end_time} - #{text} - - - END_CUE end end - end - else - # Some captions have "align:[start/end]" and "position:[num]%" - # attributes. Those are causing issues with VideoJS, which is unable - # to properly align the captions on the video, so we remove them. - # - # See: https://github.com/iv-org/invidious/issues/2391 - webvtt = YT_POOL.client &.get("#{url}&format=vtt").body - if webvtt.starts_with?(" [0-9:.]{12}).+/, "\\1") + if webvtt.starts_with?(" [0-9:.]{12}).+/, "\\1") + end end end diff --git a/src/invidious/videos/caption.cr b/src/invidious/videos/caption.cr index c85b46c3..1e2abde9 100644 --- a/src/invidious/videos/caption.cr +++ b/src/invidious/videos/caption.cr @@ -6,7 +6,9 @@ module Invidious::Videos property language_code : String property base_url : String - def initialize(@name, @language_code, @base_url) + property auto_generated : Bool + + def initialize(@name, @language_code, @base_url, @auto_generated) end # Parse the JSON structure from Youtube @@ -25,7 +27,12 @@ module Invidious::Videos language_code = caption["languageCode"].to_s base_url = caption["baseUrl"].to_s - captions_list << CaptionMetadata.new(name, language_code, base_url) + auto_generated = false + if caption["kind"]? && caption["kind"] == "asr" + auto_generated = true + end + + captions_list << CaptionMetadata.new(name, language_code, base_url, auto_generated) end return captions_list diff --git a/src/invidious/videos/transcript.cr b/src/invidious/videos/transcript.cr index ec990883..ba2728cd 100644 --- a/src/invidious/videos/transcript.cr +++ b/src/invidious/videos/transcript.cr @@ -85,7 +85,13 @@ module Invidious::Videos lines = [] of TranscriptLine body.each do |line| + # Transcript section headers. They are not apart of the captions and as such we can safely skip them. + if line.as_h.has_key?("transcriptSectionHeaderRenderer") + next + end + line = line["transcriptSegmentRenderer"] + start_ms = line["startMs"].as_s.to_i.millisecond end_ms = line["endMs"].as_s.to_i.millisecond From 3509752b791b12bcf20e12656e3b871e5034b1a7 Mon Sep 17 00:00:00 2001 From: syeopite Date: Sun, 23 Jul 2023 16:50:40 -0700 Subject: [PATCH 313/598] Rename transcript() to get_transcript() in YT API --- src/invidious/routes/api/v1/videos.cr | 2 +- src/invidious/yt_backend/youtube_api.cr | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/invidious/routes/api/v1/videos.cr b/src/invidious/routes/api/v1/videos.cr index 000e64b9..25e766d2 100644 --- a/src/invidious/routes/api/v1/videos.cr +++ b/src/invidious/routes/api/v1/videos.cr @@ -89,7 +89,7 @@ module Invidious::Routes::API::V1::Videos if CONFIG.use_innertube_for_captions params = Invidious::Videos::Transcript.generate_param(id, caption.language_code, caption.auto_generated) - initial_data = YoutubeAPI.transcript(params.to_s) + initial_data = YoutubeAPI.get_transcript(params) webvtt = Invidious::Videos::Transcript.convert_transcripts_to_vtt(initial_data, caption.language_code) else diff --git a/src/invidious/yt_backend/youtube_api.cr b/src/invidious/yt_backend/youtube_api.cr index f8aca04d..a3335bbf 100644 --- a/src/invidious/yt_backend/youtube_api.cr +++ b/src/invidious/yt_backend/youtube_api.cr @@ -558,7 +558,7 @@ module YoutubeAPI end #################################################################### - # transcript(params) + # get_transcript(params, client_config?) # # Requests the youtubei/v1/get_transcript endpoint with the required headers # and POST data in order to get a JSON reply. @@ -569,7 +569,7 @@ module YoutubeAPI # `struct ClientConfig` above for more details). # - def transcript( + def get_transcript( params : String, client_config : ClientConfig | Nil = nil ) : Hash(String, JSON::Any) From c5fe96e93603db58d6767928eedc658e8b58e59f Mon Sep 17 00:00:00 2001 From: syeopite Date: Wed, 26 Jul 2023 07:19:12 -0700 Subject: [PATCH 314/598] Remove lsquic from codebase --- config/config.example.yml | 21 --- shard.lock | 4 - shard.yml | 3 - src/invidious.cr | 2 +- src/invidious/config.cr | 2 - src/invidious/routes/images.cr | 142 +++----------------- src/invidious/yt_backend/connection_pool.cr | 37 +---- src/invidious/yt_backend/youtube_api.cr | 14 +- 8 files changed, 32 insertions(+), 193 deletions(-) diff --git a/config/config.example.yml b/config/config.example.yml index 34070fe5..e925a5e3 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -140,27 +140,6 @@ https_only: false ## #pool_size: 100 -## -## Enable/Disable the use of QUIC (HTTP/3) when connecting -## to the youtube API and websites ('youtube.com', 'ytimg.com'). -## QUIC's main advantages are its lower latency and lower bandwidth -## use, compared to its predecessors. However, the current version -## of QUIC used in invidious is still based on the IETF draft 31, -## meaning that the underlying library may still not be fully -## optimized. You can read more about QUIC at the link below: -## https://datatracker.ietf.org/doc/html/draft-ietf-quic-transport-31 -## -## Note: you should try both options and see what is the best for your -## instance. In general QUIC is recommended for public instances. Your -## mileage may vary. -## -## Note 2: Using QUIC prevents some captcha challenges from appearing. -## See: https://github.com/iv-org/invidious/issues/957#issuecomment-576424042 -## -## Accepted values: true, false -## Default: false -## -#use_quic: false ## ## Additional cookies to be sent when requesting the youtube API. diff --git a/shard.lock b/shard.lock index 235e4c25..55fcfe46 100644 --- a/shard.lock +++ b/shard.lock @@ -24,10 +24,6 @@ shards: git: https://github.com/jeromegn/kilt.git version: 0.6.1 - lsquic: - git: https://github.com/iv-org/lsquic.cr.git - version: 2.18.1-2 - pg: git: https://github.com/will/crystal-pg.git version: 0.24.0 diff --git a/shard.yml b/shard.yml index 7ee0bb2a..e929160d 100644 --- a/shard.yml +++ b/shard.yml @@ -25,9 +25,6 @@ dependencies: protodec: github: iv-org/protodec version: ~> 0.1.5 - lsquic: - github: iv-org/lsquic.cr - version: ~> 2.18.1-2 athena-negotiation: github: athena-framework/negotiation version: ~> 0.1.1 diff --git a/src/invidious.cr b/src/invidious.cr index 84e1895d..e0bd0101 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -90,7 +90,7 @@ SOFTWARE = { "branch" => "#{CURRENT_BRANCH}", } -YT_POOL = YoutubeConnectionPool.new(YT_URL, capacity: CONFIG.pool_size, use_quic: CONFIG.use_quic) +YT_POOL = YoutubeConnectionPool.new(YT_URL, capacity: CONFIG.pool_size) # CLI Kemal.config.extra_options do |parser| diff --git a/src/invidious/config.cr b/src/invidious/config.cr index e5f1e822..cee33ce1 100644 --- a/src/invidious/config.cr +++ b/src/invidious/config.cr @@ -126,8 +126,6 @@ class Config property host_binding : String = "0.0.0.0" # Pool size for HTTP requests to youtube.com and ytimg.com (each domain has a separate pool of `pool_size`) property pool_size : Int32 = 100 - # Use quic transport for youtube api - property use_quic : Bool = false # Saved cookies in "name1=value1; name2=value2..." format @[YAML::Field(converter: Preferences::StringToCookies)] diff --git a/src/invidious/routes/images.cr b/src/invidious/routes/images.cr index 594a7869..b6a2e110 100644 --- a/src/invidious/routes/images.cr +++ b/src/invidious/routes/images.cr @@ -3,17 +3,7 @@ module Invidious::Routes::Images def self.ggpht(env) url = env.request.path.lchop("/ggpht") - headers = ( - {% unless flag?(:disable_quic) %} - if CONFIG.use_quic - HTTP::Headers{":authority" => "yt3.ggpht.com"} - else - HTTP::Headers.new - end - {% else %} - HTTP::Headers.new - {% end %} - ) + headers = HTTP::Headers.new REQUEST_HEADERS_WHITELIST.each do |header| if env.request.headers[header]? @@ -42,22 +32,9 @@ module Invidious::Routes::Images } begin - {% unless flag?(:disable_quic) %} - if CONFIG.use_quic - YT_POOL.client &.get(url, headers) do |resp| - return request_proc.call(resp) - end - else - HTTP::Client.get("https://yt3.ggpht.com#{url}") do |resp| - return request_proc.call(resp) - end - end - {% else %} - # This can likely be optimized into a (small) pool sometime in the future. - HTTP::Client.get("https://yt3.ggpht.com#{url}") do |resp| - return request_proc.call(resp) - end - {% end %} + HTTP::Client.get("https://yt3.ggpht.com#{url}") do |resp| + return request_proc.call(resp) + end rescue ex end end @@ -78,10 +55,6 @@ module Invidious::Routes::Images headers = HTTP::Headers.new - {% unless flag?(:disable_quic) %} - headers[":authority"] = "#{authority}.ytimg.com" - {% end %} - REQUEST_HEADERS_WHITELIST.each do |header| if env.request.headers[header]? headers[header] = env.request.headers[header] @@ -107,22 +80,9 @@ module Invidious::Routes::Images } begin - {% unless flag?(:disable_quic) %} - if CONFIG.use_quic - YT_POOL.client &.get(url, headers) do |resp| - return request_proc.call(resp) - end - else - HTTP::Client.get("https://#{authority}.ytimg.com#{url}") do |resp| - return request_proc.call(resp) - end - end - {% else %} - # This can likely be optimized into a (small) pool sometime in the future. - HTTP::Client.get("https://#{authority}.ytimg.com#{url}") do |resp| - return request_proc.call(resp) - end - {% end %} + HTTP::Client.get("https://#{authority}.ytimg.com#{url}") do |resp| + return request_proc.call(resp) + end rescue ex end end @@ -133,17 +93,7 @@ module Invidious::Routes::Images name = env.params.url["name"] url = env.request.resource - headers = ( - {% unless flag?(:disable_quic) %} - if CONFIG.use_quic - HTTP::Headers{":authority" => "i9.ytimg.com"} - else - HTTP::Headers.new - end - {% else %} - HTTP::Headers.new - {% end %} - ) + headers = HTTP::Headers.new REQUEST_HEADERS_WHITELIST.each do |header| if env.request.headers[header]? @@ -169,22 +119,9 @@ module Invidious::Routes::Images } begin - {% unless flag?(:disable_quic) %} - if CONFIG.use_quic - YT_POOL.client &.get(url, headers) do |resp| - return request_proc.call(resp) - end - else - HTTP::Client.get("https://i9.ytimg.com#{url}") do |resp| - return request_proc.call(resp) - end - end - {% else %} - # This can likely be optimized into a (small) pool sometime in the future. - HTTP::Client.get("https://i9.ytimg.com#{url}") do |resp| - return request_proc.call(resp) - end - {% end %} + HTTP::Client.get("https://i9.ytimg.com#{url}") do |resp| + return request_proc.call(resp) + end rescue ex end end @@ -223,41 +160,16 @@ module Invidious::Routes::Images id = env.params.url["id"] name = env.params.url["name"] - headers = ( - {% unless flag?(:disable_quic) %} - if CONFIG.use_quic - HTTP::Headers{":authority" => "i.ytimg.com"} - else - HTTP::Headers.new - end - {% else %} - HTTP::Headers.new - {% end %} - ) + headers = HTTP::Headers.new if name == "maxres.jpg" build_thumbnails(id).each do |thumb| thumbnail_resource_path = "/vi/#{id}/#{thumb[:url]}.jpg" - # Logic here is short enough that manually typing them out should be fine. - {% unless flag?(:disable_quic) %} - if CONFIG.use_quic - if YT_POOL.client &.head(thumbnail_resource_path, headers).status_code == 200 - name = thumb[:url] + ".jpg" - break - end - else - if HTTP::Client.head("https://i.ytimg.com#{thumbnail_resource_path}").status_code == 200 - name = thumb[:url] + ".jpg" - break - end - end - {% else %} - # This can likely be optimized into a (small) pool sometime in the future. - if HTTP::Client.head("https://i.ytimg.com#{thumbnail_resource_path}").status_code == 200 - name = thumb[:url] + ".jpg" - break - end - {% end %} + # This can likely be optimized into a (small) pool sometime in the future. + if HTTP::Client.head("https://i.ytimg.com#{thumbnail_resource_path}").status_code == 200 + name = thumb[:url] + ".jpg" + break + end end end @@ -287,22 +199,10 @@ module Invidious::Routes::Images } begin - {% unless flag?(:disable_quic) %} - if CONFIG.use_quic - YT_POOL.client &.get(url, headers) do |resp| - return request_proc.call(resp) - end - else - HTTP::Client.get("https://i.ytimg.com#{url}") do |resp| - return request_proc.call(resp) - end - end - {% else %} - # This can likely be optimized into a (small) pool sometime in the future. - HTTP::Client.get("https://i.ytimg.com#{url}") do |resp| - return request_proc.call(resp) - end - {% end %} + # This can likely be optimized into a (small) pool sometime in the future. + HTTP::Client.get("https://i.ytimg.com#{url}") do |resp| + return request_proc.call(resp) + end rescue ex end end diff --git a/src/invidious/yt_backend/connection_pool.cr b/src/invidious/yt_backend/connection_pool.cr index 658731cf..e9eb726c 100644 --- a/src/invidious/yt_backend/connection_pool.cr +++ b/src/invidious/yt_backend/connection_pool.cr @@ -1,11 +1,3 @@ -{% unless flag?(:disable_quic) %} - require "lsquic" - - alias HTTPClientType = QUIC::Client | HTTP::Client -{% else %} - alias HTTPClientType = HTTP::Client -{% end %} - def add_yt_headers(request) if request.headers["User-Agent"] == "Crystal" request.headers["User-Agent"] ||= "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" @@ -26,11 +18,11 @@ struct YoutubeConnectionPool property! url : URI property! capacity : Int32 property! timeout : Float64 - property pool : DB::Pool(HTTPClientType) + property pool : DB::Pool(HTTP::Client) - def initialize(url : URI, @capacity = 5, @timeout = 5.0, use_quic = true) + def initialize(url : URI, @capacity = 5, @timeout = 5.0) @url = url - @pool = build_pool(use_quic) + @pool = build_pool() end def client(region = nil, &block) @@ -43,11 +35,7 @@ struct YoutubeConnectionPool response = yield conn rescue ex conn.close - {% unless flag?(:disable_quic) %} - conn = CONFIG.use_quic ? QUIC::Client.new(url) : HTTP::Client.new(url) - {% else %} - conn = HTTP::Client.new(url) - {% end %} + conn = HTTP::Client.new(url) conn.family = (url.host == "www.youtube.com") ? CONFIG.force_resolve : Socket::Family::INET conn.family = Socket::Family::INET if conn.family == Socket::Family::UNSPEC @@ -61,19 +49,9 @@ struct YoutubeConnectionPool response end - private def build_pool(use_quic) - DB::Pool(HTTPClientType).new(initial_pool_size: 0, max_pool_size: capacity, max_idle_pool_size: capacity, checkout_timeout: timeout) do - conn = nil # Declare - {% unless flag?(:disable_quic) %} - if use_quic - conn = QUIC::Client.new(url) - else - conn = HTTP::Client.new(url) - end - {% else %} - conn = HTTP::Client.new(url) - {% end %} - + private def build_pool + DB::Pool(HTTP::Client).new(initial_pool_size: 0, max_pool_size: capacity, max_idle_pool_size: capacity, checkout_timeout: timeout) do + conn = HTTP::Client.new(url) conn.family = (url.host == "www.youtube.com") ? CONFIG.force_resolve : Socket::Family::INET conn.family = Socket::Family::INET if conn.family == Socket::Family::UNSPEC conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com" @@ -83,7 +61,6 @@ struct YoutubeConnectionPool end def make_client(url : URI, region = nil) - # TODO: Migrate any applicable endpoints to QUIC client = HTTPClient.new(url, OpenSSL::SSL::Context::Client.insecure) client.family = (url.host == "www.youtube.com") ? CONFIG.force_resolve : Socket::Family::UNSPEC client.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com" diff --git a/src/invidious/yt_backend/youtube_api.cr b/src/invidious/yt_backend/youtube_api.cr index 3dd9e9d8..aef9ddd9 100644 --- a/src/invidious/yt_backend/youtube_api.cr +++ b/src/invidious/yt_backend/youtube_api.cr @@ -595,17 +595,9 @@ module YoutubeAPI LOGGER.trace("YoutubeAPI: POST data: #{data}") # Send the POST request - if {{ !flag?(:disable_quic) }} && CONFIG.use_quic - # Using QUIC client - body = YT_POOL.client(client_config.proxy_region, - &.post(url, headers: headers, body: data.to_json) - ).body - else - # Using HTTP client - body = YT_POOL.client(client_config.proxy_region) do |client| - client.post(url, headers: headers, body: data.to_json) do |response| - self._decompress(response.body_io, response.headers["Content-Encoding"]?) - end + body = YT_POOL.client(client_config.proxy_region) do |client| + client.post(url, headers: headers, body: data.to_json) do |response| + self._decompress(response.body_io, response.headers["Content-Encoding"]?) end end From a8ba02051b261a634050ea7f621451d84ca61607 Mon Sep 17 00:00:00 2001 From: syeopite Date: Wed, 26 Jul 2023 07:25:19 -0700 Subject: [PATCH 315/598] Remove(?) lsquic from make and docker files --- .github/workflows/container-release.yml | 29 ++----------------------- Makefile | 6 ----- docker/Dockerfile | 11 +--------- docker/Dockerfile.arm64 | 11 +--------- 4 files changed, 4 insertions(+), 53 deletions(-) diff --git a/.github/workflows/container-release.yml b/.github/workflows/container-release.yml index 86aec94f..13bbf34c 100644 --- a/.github/workflows/container-release.yml +++ b/.github/workflows/container-release.yml @@ -52,7 +52,7 @@ jobs: username: ${{ secrets.QUAY_USERNAME }} password: ${{ secrets.QUAY_PASSWORD }} - - name: Build and push Docker AMD64 image without QUIC for Push Event + - name: Build and push Docker AMD64 image for Push Event if: github.ref == 'refs/heads/master' uses: docker/build-push-action@v3 with: @@ -64,9 +64,8 @@ jobs: tags: quay.io/invidious/invidious:${{ github.sha }},quay.io/invidious/invidious:latest build-args: | "release=1" - "disable_quic=1" - - name: Build and push Docker ARM64 image without QUIC for Push Event + - name: Build and push Docker ARM64 image for Push Event if: github.ref == 'refs/heads/master' uses: docker/build-push-action@v3 with: @@ -78,28 +77,4 @@ jobs: tags: quay.io/invidious/invidious:${{ github.sha }}-arm64,quay.io/invidious/invidious:latest-arm64 build-args: | "release=1" - "disable_quic=1" - - name: Build and push Docker AMD64 image with QUIC for Push Event - if: github.ref == 'refs/heads/master' - uses: docker/build-push-action@v3 - with: - context: . - file: docker/Dockerfile - platforms: linux/amd64 - labels: quay.expires-after=12w - push: true - tags: quay.io/invidious/invidious:${{ github.sha }}-quic,quay.io/invidious/invidious:latest-quic - build-args: release=1 - - - name: Build and push Docker ARM64 image with QUIC for Push Event - if: github.ref == 'refs/heads/master' - uses: docker/build-push-action@v3 - with: - context: . - file: docker/Dockerfile.arm64 - platforms: linux/arm64/v8 - labels: quay.expires-after=12w - push: true - tags: quay.io/invidious/invidious:${{ github.sha }}-arm64-quic,quay.io/invidious/invidious:latest-arm64-quic - build-args: release=1 diff --git a/Makefile b/Makefile index d4657792..9eb195df 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,6 @@ RELEASE := 1 STATIC := 0 -DISABLE_QUIC := 1 NO_DBG_SYMBOLS := 0 @@ -27,10 +26,6 @@ else FLAGS += --debug endif -ifeq ($(DISABLE_QUIC), 1) - FLAGS += -Ddisable_quic -endif - ifeq ($(API_ONLY), 1) FLAGS += -Dapi_only endif @@ -115,7 +110,6 @@ help: @echo " STATIC Link libraries statically (Default: 0)" @echo "" @echo " API_ONLY Build invidious without a GUI (Default: 0)" - @echo " DISABLE_QUIC Disable support for QUIC (Default: 0)" @echo " NO_DBG_SYMBOLS Strip debug symbols (Default: 0)" diff --git a/docker/Dockerfile b/docker/Dockerfile index 57864883..761bbdca 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -2,15 +2,12 @@ FROM crystallang/crystal:1.4.1-alpine AS builder RUN apk add --no-cache sqlite-static yaml-static ARG release -ARG disable_quic WORKDIR /invidious COPY ./shard.yml ./shard.yml COPY ./shard.lock ./shard.lock RUN shards install --production -COPY --from=quay.io/invidious/lsquic-compiled /root/liblsquic.a ./lib/lsquic/src/lsquic/ext/liblsquic.a - COPY ./src/ ./src/ # TODO: .git folder is required for building – this is destructive. # See definition of CURRENT_BRANCH, CURRENT_COMMIT and CURRENT_VERSION. @@ -24,13 +21,7 @@ COPY ./videojs-dependencies.yml ./videojs-dependencies.yml RUN crystal spec --warnings all \ --link-flags "-lxml2 -llzma" -RUN if [[ "${release}" == 1 && "${disable_quic}" == 1 ]] ; then \ - crystal build ./src/invidious.cr \ - --release \ - -Ddisable_quic \ - --static --warnings all \ - --link-flags "-lxml2 -llzma"; \ - elif [[ "${release}" == 1 ]] ; then \ +RUN if [[ "${release}" == 1 ]] ; then \ crystal build ./src/invidious.cr \ --release \ --static --warnings all \ diff --git a/docker/Dockerfile.arm64 b/docker/Dockerfile.arm64 index 10135efb..cf9231fb 100644 --- a/docker/Dockerfile.arm64 +++ b/docker/Dockerfile.arm64 @@ -2,15 +2,12 @@ FROM alpine:3.16 AS builder RUN apk add --no-cache 'crystal=1.4.1-r0' shards sqlite-static yaml-static yaml-dev libxml2-dev zlib-static openssl-libs-static openssl-dev musl-dev ARG release -ARG disable_quic WORKDIR /invidious COPY ./shard.yml ./shard.yml COPY ./shard.lock ./shard.lock RUN shards install --production -COPY --from=quay.io/invidious/lsquic-compiled /root/liblsquic.a ./lib/lsquic/src/lsquic/ext/liblsquic.a - COPY ./src/ ./src/ # TODO: .git folder is required for building – this is destructive. # See definition of CURRENT_BRANCH, CURRENT_COMMIT and CURRENT_VERSION. @@ -24,13 +21,7 @@ COPY ./videojs-dependencies.yml ./videojs-dependencies.yml RUN crystal spec --warnings all \ --link-flags "-lxml2 -llzma" -RUN if [[ "${release}" == 1 && "${disable_quic}" == 1 ]] ; then \ - crystal build ./src/invidious.cr \ - --release \ - -Ddisable_quic \ - --static --warnings all \ - --link-flags "-lxml2 -llzma"; \ - elif [[ "${release}" == 1 ]] ; then \ +RUN if [[ "${release}" == 1 ]] ; then \ crystal build ./src/invidious.cr \ --release \ --static --warnings all \ From 70b80ce8ad5ad9e5eb57a8f2f8e72a2274f8523f Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Fri, 28 Jul 2023 08:11:15 +0200 Subject: [PATCH 316/598] I18n: Add translation strings for new feature (fr/en) --- locales/en-US.json | 2 ++ locales/fr.json | 2 ++ 2 files changed, 4 insertions(+) diff --git a/locales/en-US.json b/locales/en-US.json index 74f43d90..06d095dc 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -1,4 +1,6 @@ { + "generic_channels_count": "{{count}} channel", + "generic_channels_count_plural": "{{count}} channels", "generic_views_count": "{{count}} view", "generic_views_count_plural": "{{count}} views", "generic_videos_count": "{{count}} video", diff --git a/locales/fr.json b/locales/fr.json index 2eb4dd2b..c48c8be5 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -1,4 +1,6 @@ { + "generic_channels_count": "{{count}} chaîne", + "generic_channels_count_plural": "{{count}} chaînes", "generic_views_count": "{{count}} vue", "generic_views_count_plural": "{{count}} vues", "generic_videos_count": "{{count}} vidéo", From 0d27eef047d24f8c7b3f9528502bc5828cad3c73 Mon Sep 17 00:00:00 2001 From: Fabio Henrique Date: Sun, 6 Aug 2023 12:29:19 +0000 Subject: [PATCH 317/598] update ameba version fix shard.yml authors typo --- shard.lock | 7 ++++--- shard.yml | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/shard.lock b/shard.lock index 55fcfe46..efb60a59 100644 --- a/shard.lock +++ b/shard.lock @@ -1,5 +1,9 @@ version: 2.0 shards: + ameba: + git: https://github.com/crystal-ameba/ameba.git + version: 1.5.0 + athena-negotiation: git: https://github.com/athena-framework/negotiation.git version: 0.1.1 @@ -44,6 +48,3 @@ shards: git: https://github.com/crystal-lang/crystal-sqlite3.git version: 0.18.0 - ameba: - git: https://github.com/crystal-ameba/ameba.git - version: 0.14.3 diff --git a/shard.yml b/shard.yml index e929160d..be06a7df 100644 --- a/shard.yml +++ b/shard.yml @@ -3,7 +3,7 @@ version: 0.20.1 authors: - Omar Roth - - Invidous team + - Invidious team targets: invidious: @@ -35,7 +35,7 @@ development_dependencies: version: ~> 0.10.4 ameba: github: crystal-ameba/ameba - version: ~> 0.14.3 + version: ~> 1.5.0 crystal: ">= 1.0.0, < 2.0.0" From 2f6b2688bb8042c29942e46767dc78836f21fb57 Mon Sep 17 00:00:00 2001 From: syeopite Date: Sun, 6 Aug 2023 12:20:05 -0700 Subject: [PATCH 318/598] Use workaround for fetching streaming URLs YouTube appears to be A/B testing some new integrity checks. Adding the parameter "CgIQBg" to InnerTube player requests appears to workaround the problem See https://github.com/TeamNewPipe/NewPipeExtractor/pull/1084 --- src/invidious/videos/parser.cr | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr index 9cc0ffdc..2a09d187 100644 --- a/src/invidious/videos/parser.cr +++ b/src/invidious/videos/parser.cr @@ -55,8 +55,9 @@ def extract_video_info(video_id : String, proxy_region : String? = nil) client_config = YoutubeAPI::ClientConfig.new(proxy_region: proxy_region) # Fetch data from the player endpoint - # 8AEB param is used to fetch YouTube stories - player_response = YoutubeAPI.player(video_id: video_id, params: "8AEB", client_config: client_config) + # CgIQBg is a workaround for streaming URLs that returns a 403. + # See https://github.com/iv-org/invidious/issues/4027#issuecomment-1666944520 + player_response = YoutubeAPI.player(video_id: video_id, params: "CgIQBg", client_config: client_config) playability_status = player_response.dig?("playabilityStatus", "status").try &.as_s @@ -135,8 +136,9 @@ end def try_fetch_streaming_data(id : String, client_config : YoutubeAPI::ClientConfig) : Hash(String, JSON::Any)? LOGGER.debug("try_fetch_streaming_data: [#{id}] Using #{client_config.client_type} client.") - # 8AEB param is used to fetch YouTube stories - response = YoutubeAPI.player(video_id: id, params: "8AEB", client_config: client_config) + # CgIQBg is a workaround for streaming URLs that returns a 403. + # See https://github.com/iv-org/invidious/issues/4027#issuecomment-1666944520 + response = YoutubeAPI.player(video_id: id, params: "CgIQBg", client_config: client_config) playability_status = response["playabilityStatus"]["status"] LOGGER.debug("try_fetch_streaming_data: [#{id}] Got playabilityStatus == #{playability_status}.") From 71693ba6063c06efd1b9780313246b8dbc020f72 Mon Sep 17 00:00:00 2001 From: atilluF <110931720+atilluF@users.noreply.github.com> Date: Mon, 10 Jul 2023 17:50:47 +0000 Subject: [PATCH 319/598] Update Italian translation --- locales/it.json | 75 +++++++++++++++++++++++++++++-------------------- 1 file changed, 45 insertions(+), 30 deletions(-) diff --git a/locales/it.json b/locales/it.json index a3d0f5da..9d633264 100644 --- a/locales/it.json +++ b/locales/it.json @@ -1,10 +1,13 @@ { - "generic_subscribers_count": "{{count}} iscritto", - "generic_subscribers_count_plural": "{{count}} iscritti", - "generic_videos_count": "{{count}} video", - "generic_videos_count_plural": "{{count}} video", - "generic_playlists_count": "{{count}} playlist", - "generic_playlists_count_plural": "{{count}} playlist", + "generic_subscribers_count_0": "{{count}} iscritto", + "generic_subscribers_count_1": "{{count}} iscritti", + "generic_subscribers_count_2": "{{count}} iscritti", + "generic_videos_count_0": "{{count}} video", + "generic_videos_count_1": "{{count}} video", + "generic_videos_count_2": "{{count}} video", + "generic_playlists_count_0": "{{count}} playlist", + "generic_playlists_count_1": "{{count}} playlist", + "generic_playlists_count_2": "{{count}} playlist", "LIVE": "IN DIRETTA", "Shared `x` ago": "Condiviso `x` fa", "Unsubscribe": "Disiscriviti", @@ -113,16 +116,18 @@ "Subscription manager": "Gestione delle iscrizioni", "Token manager": "Gestione dei gettoni", "Token": "Gettone", - "generic_subscriptions_count": "{{count}} iscrizione", - "generic_subscriptions_count_plural": "{{count}} iscrizioni", + "generic_subscriptions_count_0": "{{count}} iscrizione", + "generic_subscriptions_count_1": "{{count}} iscrizioni", + "generic_subscriptions_count_2": "{{count}} iscrizioni", "tokens_count": "{{count}} gettone", "tokens_count_plural": "{{count}} gettoni", "Import/export": "Importa/esporta", "unsubscribe": "disiscriviti", "revoke": "revoca", "Subscriptions": "Iscrizioni", - "subscriptions_unseen_notifs_count": "{{count}} notifica non visualizzata", - "subscriptions_unseen_notifs_count_plural": "{{count}} notifiche non visualizzate", + "subscriptions_unseen_notifs_count_0": "{{count}} notifica non visualizzata", + "subscriptions_unseen_notifs_count_1": "{{count}} notifiche non visualizzate", + "subscriptions_unseen_notifs_count_2": "{{count}} notifiche non visualizzate", "search": "Cerca", "Log out": "Esci", "Source available here.": "Codice sorgente.", @@ -151,8 +156,9 @@ "Whitelisted regions: ": "Regioni in lista bianca: ", "Blacklisted regions: ": "Regioni in lista nera: ", "Shared `x`": "Condiviso `x`", - "generic_views_count": "{{count}} visualizzazione", - "generic_views_count_plural": "{{count}} visualizzazioni", + "generic_views_count_0": "{{count}} visualizzazione", + "generic_views_count_1": "{{count}} visualizzazioni", + "generic_views_count_2": "{{count}} visualizzazioni", "Premieres in `x`": "In anteprima in `x`", "Premieres `x`": "In anteprima `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.": "Ciao, Sembra che tu abbia disattivato JavaScript. Clicca qui per visualizzare i commenti, ma considera che il caricamento potrebbe richiedere più tempo.", @@ -300,20 +306,27 @@ "Yiddish": "Yiddish", "Yoruba": "Yoruba", "Zulu": "Zulu", - "generic_count_years": "{{count}} anno", - "generic_count_years_plural": "{{count}} anni", - "generic_count_months": "{{count}} mese", - "generic_count_months_plural": "{{count}} mesi", - "generic_count_weeks": "{{count}} settimana", - "generic_count_weeks_plural": "{{count}} settimane", - "generic_count_days": "{{count}} giorno", - "generic_count_days_plural": "{{count}} giorni", - "generic_count_hours": "{{count}} ora", - "generic_count_hours_plural": "{{count}} ore", - "generic_count_minutes": "{{count}} minuto", - "generic_count_minutes_plural": "{{count}} minuti", - "generic_count_seconds": "{{count}} secondo", - "generic_count_seconds_plural": "{{count}} secondi", + "generic_count_years_0": "{{count}} anno", + "generic_count_years_1": "{{count}} anni", + "generic_count_years_2": "{{count}} anni", + "generic_count_months_0": "{{count}} mese", + "generic_count_months_1": "{{count}} mesi", + "generic_count_months_2": "{{count}} mesi", + "generic_count_weeks_0": "{{count}} settimana", + "generic_count_weeks_1": "{{count}} settimane", + "generic_count_weeks_2": "{{count}} settimane", + "generic_count_days_0": "{{count}} giorno", + "generic_count_days_1": "{{count}} giorni", + "generic_count_days_2": "{{count}} giorni", + "generic_count_hours_0": "{{count}} ora", + "generic_count_hours_1": "{{count}} ore", + "generic_count_hours_2": "{{count}} ore", + "generic_count_minutes_0": "{{count}} minuto", + "generic_count_minutes_1": "{{count}} minuti", + "generic_count_minutes_2": "{{count}} minuti", + "generic_count_seconds_0": "{{count}} secondo", + "generic_count_seconds_1": "{{count}} secondi", + "generic_count_seconds_2": "{{count}} secondi", "Fallback comments: ": "Commenti alternativi: ", "Popular": "Popolare", "Search": "Cerca", @@ -417,10 +430,12 @@ "search_filters_duration_option_short": "Corto (< 4 minuti)", "search_filters_duration_option_long": "Lungo (> 20 minuti)", "search_filters_features_option_purchased": "Acquistato", - "comments_view_x_replies": "Vedi {{count}} risposta", - "comments_view_x_replies_plural": "Vedi {{count}} risposte", - "comments_points_count": "{{count}} punto", - "comments_points_count_plural": "{{count}} punti", + "comments_view_x_replies_0": "Vedi {{count}} risposta", + "comments_view_x_replies_1": "Vedi {{count}} risposte", + "comments_view_x_replies_2": "Vedi {{count}} risposte", + "comments_points_count_0": "{{count}} punto", + "comments_points_count_1": "{{count}} punti", + "comments_points_count_2": "{{count}} punti", "Portuguese (auto-generated)": "Portoghese (generati automaticamente)", "crash_page_you_found_a_bug": "Sembra che tu abbia trovato un bug in Invidious!", "crash_page_switch_instance": "provato a usare un'altra istanza", From 0697b3787ff19939fda1bc5c12ada8729dbf960a Mon Sep 17 00:00:00 2001 From: Jorge Maldonado Ventura Date: Sun, 9 Jul 2023 22:14:47 +0000 Subject: [PATCH 320/598] Update Esperanto translation --- locales/eo.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/eo.json b/locales/eo.json index a4b46bef..e2a7b7b1 100644 --- a/locales/eo.json +++ b/locales/eo.json @@ -447,8 +447,8 @@ "French (auto-generated)": "Franca (aŭtomate generita)", "Spanish (Mexico)": "Hispana (Meksiko)", "Spanish (auto-generated)": "Hispana (aŭtomate generita)", - "generic_count_days": "{{count}} jaro", - "generic_count_days_plural": "{{count}} jaroj", + "generic_count_days": "{{count}} tago", + "generic_count_days_plural": "{{count}} tagoj", "search_filters_type_option_all": "Ajna speco", "search_filters_duration_option_none": "Ajna daŭro", "search_filters_apply_button": "Uzi elektitajn filtrilojn", From cb09f46e04c91a0e02073228dc720c572b69aad1 Mon Sep 17 00:00:00 2001 From: CRW Date: Thu, 13 Jul 2023 14:10:15 +0200 Subject: [PATCH 321/598] Add Latin translation --- locales/la.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 locales/la.json diff --git a/locales/la.json b/locales/la.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/locales/la.json @@ -0,0 +1 @@ +{} From 1837467aeb77d57c57f5e7ccf81693d61d7c2d69 Mon Sep 17 00:00:00 2001 From: maboroshin Date: Thu, 13 Jul 2023 00:17:04 +0000 Subject: [PATCH 322/598] Update Japanese translation --- locales/ja.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/locales/ja.json b/locales/ja.json index 8adcbf6a..b489ece0 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -366,13 +366,13 @@ "next_steps_error_message": "下記のものを試して下さい: ", "next_steps_error_message_refresh": "再読込", "next_steps_error_message_go_to_youtube": "YouTubeへ", - "search_filters_duration_option_short": "4 分未満", + "search_filters_duration_option_short": "4分未満", "footer_documentation": "説明書", "footer_source_code": "ソースコード", "footer_original_source_code": "元のソースコード", "footer_modfied_source_code": "改変して使用", "adminprefs_modified_source_code_url_label": "改変されたソースコードのレポジトリのURL", - "search_filters_duration_option_long": "20 分以上", + "search_filters_duration_option_long": "20分以上", "preferences_region_label": "地域: ", "footer_donate_page": "寄付する", "preferences_quality_dash_label": "優先するDASH画質: ", @@ -443,7 +443,7 @@ "search_filters_date_option_none": "すべて", "search_filters_type_option_all": "すべての種類", "search_filters_duration_option_none": "すべての長さ", - "search_filters_duration_option_medium": "4 ~ 20 分", + "search_filters_duration_option_medium": "4 ~ 20分", "preferences_save_player_pos_label": "再生位置を保存: ", "crash_page_before_reporting": "バグを報告する前に、次のことを確認してください。", "crash_page_report_issue": "上記が助けにならないなら、GitHub に新しい issue を作成し(英語が好ましい)、メッセージに次のテキストを含めてください(テキストは翻訳しない)。", From ab475718c8b2c3fb87cf718e39cfcab3b21312ef Mon Sep 17 00:00:00 2001 From: Eryk Michalak Date: Sat, 15 Jul 2023 08:33:40 +0000 Subject: [PATCH 323/598] Update Polish translation --- locales/pl.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/pl.json b/locales/pl.json index e237db8b..6337465b 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -148,12 +148,12 @@ "Blacklisted regions: ": "Niedostępny na obszarach: ", "Shared `x`": "Udostępniono `x`", "Premieres in `x`": "Publikacja za `x`", - "Premieres `x`": "Publikacja za `x`", + "Premieres `x`": "Publikacja `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.": "Cześć! Wygląda na to, że masz wyłączoną obsługę JavaScriptu. Kliknij tutaj, żeby zobaczyć komentarze. Pamiętaj, że wczytywanie może potrwać dłużej.", "View YouTube comments": "Wyświetl komentarze z YouTube", "View more comments on Reddit": "Wyświetl więcej komentarzy na Reddicie", "View `x` comments": { - "([^.,0-9]|^)1([^.,0-9]|$)": "Wyświetl `x` komentarzy", + "([^.,0-9]|^)1([^.,0-9]|$)": "Wyświetl `x` komentarz", "": "Wyświetl `x` komentarzy" }, "View Reddit comments": "Wyświetl komentarze z Redditta", From f993b1e119ac4284ae1e94c1504c31ba8c06b0a6 Mon Sep 17 00:00:00 2001 From: Rex_sa Date: Sun, 16 Jul 2023 15:41:28 +0000 Subject: [PATCH 324/598] Update Arabic translation --- locales/ar.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/locales/ar.json b/locales/ar.json index c137d1a3..877fb9ff 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -540,5 +540,13 @@ "Channel Sponsor": "راعي القناة", "Standard YouTube license": "ترخيص YouTube القياسي", "Download is disabled": "تم تعطيل التحميلات", - "Import YouTube playlist (.csv)": "استيراد قائمة تشغيل YouTube (.csv)" + "Import YouTube playlist (.csv)": "استيراد قائمة تشغيل YouTube (.csv)", + "generic_button_save": "حفظ", + "generic_button_delete": "حذف", + "generic_button_edit": "تحرير", + "generic_button_cancel": "الغاء", + "generic_button_rss": "RSS", + "channel_tab_releases_label": "الإصدارات", + "playlist_button_add_items": "إضافة مقاطع فيديو", + "channel_tab_podcasts_label": "البودكاست" } From 7a5f5173ddebd9c3286ac0e7b80bca5004993040 Mon Sep 17 00:00:00 2001 From: Jorge Maldonado Ventura Date: Sun, 16 Jul 2023 16:10:15 +0000 Subject: [PATCH 325/598] Update Spanish translation --- locales/es.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/locales/es.json b/locales/es.json index b3103a25..f1697d30 100644 --- a/locales/es.json +++ b/locales/es.json @@ -476,5 +476,13 @@ "Channel Sponsor": "Patrocinador del canal", "Standard YouTube license": "Licencia de YouTube estándar", "Download is disabled": "La descarga está deshabilitada", - "Import YouTube playlist (.csv)": "Importar lista de reproducción de YouTube (.csv)" + "Import YouTube playlist (.csv)": "Importar lista de reproducción de YouTube (.csv)", + "playlist_button_add_items": "Añadir vídeos", + "generic_button_edit": "Editar", + "generic_button_save": "Guardar", + "generic_button_delete": "Borrar", + "generic_button_cancel": "Cancelar", + "generic_button_rss": "RSS", + "channel_tab_podcasts_label": "Podcasts", + "channel_tab_releases_label": "Publicaciones" } From e3fe6c44f88c934b2066e1a2909002c5e35ee1c8 Mon Sep 17 00:00:00 2001 From: Matthaiks Date: Sun, 16 Jul 2023 16:48:29 +0000 Subject: [PATCH 326/598] Update Polish translation --- locales/pl.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/locales/pl.json b/locales/pl.json index 6337465b..f1924c8a 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -492,5 +492,13 @@ "Song: ": "Piosenka: ", "Channel Sponsor": "Sponsor kanału", "Standard YouTube license": "Standardowa licencja YouTube", - "Import YouTube playlist (.csv)": "Importuj playlistę YouTube (.csv)" + "Import YouTube playlist (.csv)": "Importuj playlistę YouTube (.csv)", + "generic_button_edit": "Edytuj", + "generic_button_cancel": "Anuluj", + "generic_button_rss": "RSS", + "channel_tab_podcasts_label": "Podkasty", + "channel_tab_releases_label": "Wydania", + "generic_button_delete": "Usuń", + "generic_button_save": "Zapisz", + "playlist_button_add_items": "Dodaj filmy" } From a5a5422014aa4723c6d0c4d83de554127608a783 Mon Sep 17 00:00:00 2001 From: Jorge Maldonado Ventura Date: Sun, 16 Jul 2023 16:16:04 +0000 Subject: [PATCH 327/598] Update Spanish translation --- locales/es.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/es.json b/locales/es.json index f1697d30..b4a56030 100644 --- a/locales/es.json +++ b/locales/es.json @@ -113,7 +113,7 @@ "Token manager": "Gestor de tokens", "Token": "Ficha", "Import/export": "Importar/Exportar", - "unsubscribe": "Desuscribirse", + "unsubscribe": "desuscribirse", "revoke": "revocar", "Subscriptions": "Suscripciones", "search": "buscar", @@ -154,7 +154,7 @@ "View YouTube comments": "Ver los comentarios de YouTube", "View more comments on Reddit": "Ver más comentarios en Reddit", "View `x` comments": { - "([^.,0-9]|^)1([^.,0-9]|$)": "Ver `x` comentarios", + "([^.,0-9]|^)1([^.,0-9]|$)": "Ver `x` comentario", "": "Ver `x` comentarios" }, "View Reddit comments": "Ver los comentarios de Reddit", From 552893a3c1e19f473003d0b5694d7e7af03238c9 Mon Sep 17 00:00:00 2001 From: Jorge Maldonado Ventura Date: Sun, 16 Jul 2023 16:18:13 +0000 Subject: [PATCH 328/598] Update Esperanto translation --- locales/eo.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/locales/eo.json b/locales/eo.json index e2a7b7b1..6d1b0bc1 100644 --- a/locales/eo.json +++ b/locales/eo.json @@ -154,7 +154,7 @@ "View YouTube comments": "Vidi komentojn de JuTubo", "View more comments on Reddit": "Vidi pli komentoj en Reddit", "View `x` comments": { - "([^.,0-9]|^)1([^.,0-9]|$)": "Vidi `x` komentojn", + "([^.,0-9]|^)1([^.,0-9]|$)": "Vidi `x` komenton", "": "Vidi `x` komentojn" }, "View Reddit comments": "Vidi komentojn de Reddit", @@ -476,5 +476,13 @@ "Song: ": "Muzikaĵo: ", "Standard YouTube license": "Implicita YouTube-licenco", "Download is disabled": "Elŝuto estas malebligita", - "Import YouTube playlist (.csv)": "Importi YouTube-ludliston (.csv)" + "Import YouTube playlist (.csv)": "Importi YouTube-ludliston (.csv)", + "generic_button_edit": "Redakti", + "playlist_button_add_items": "Aldoni videojn", + "generic_button_rss": "RSS", + "generic_button_delete": "Forigi", + "channel_tab_podcasts_label": "Podkastoj", + "generic_button_cancel": "Nuligi", + "channel_tab_releases_label": "Eldonoj", + "generic_button_save": "Konservi" } From 625d8c00ba063539719fb92fd986ef9aafd3cc86 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Sun, 16 Jul 2023 21:12:19 +0000 Subject: [PATCH 329/598] Update Ukrainian translation --- locales/uk.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/locales/uk.json b/locales/uk.json index 308b10ca..4d8f06a5 100644 --- a/locales/uk.json +++ b/locales/uk.json @@ -492,5 +492,13 @@ "Channel Sponsor": "Спонсор каналу", "Standard YouTube license": "Стандартна ліцензія YouTube", "Download is disabled": "Завантаження вимкнено", - "Import YouTube playlist (.csv)": "Імпорт списку відтворення YouTube (.csv)" + "Import YouTube playlist (.csv)": "Імпорт списку відтворення YouTube (.csv)", + "channel_tab_podcasts_label": "Подкасти", + "playlist_button_add_items": "Додати відео", + "generic_button_cancel": "Скасувати", + "generic_button_rss": "RSS", + "channel_tab_releases_label": "Випуски", + "generic_button_delete": "Видалити", + "generic_button_edit": "Змінити", + "generic_button_save": "Зберегти" } From d7d95fd725f3f79d35c34a0b0219a85e3fa2ee9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?O=C4=9Fuz=20Ersen?= Date: Sun, 16 Jul 2023 18:27:44 +0000 Subject: [PATCH 330/598] Update Turkish translation --- locales/tr.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/locales/tr.json b/locales/tr.json index 22732a51..7f3f2de8 100644 --- a/locales/tr.json +++ b/locales/tr.json @@ -476,5 +476,13 @@ "Song: ": "Şarkı: ", "Standard YouTube license": "Standart YouTube lisansı", "Download is disabled": "İndirme devre dışı", - "Import YouTube playlist (.csv)": "YouTube Oynatma Listesini İçe Aktar (.csv)" + "Import YouTube playlist (.csv)": "YouTube Oynatma Listesini İçe Aktar (.csv)", + "generic_button_delete": "Sil", + "generic_button_edit": "Düzenle", + "generic_button_save": "Kaydet", + "generic_button_cancel": "İptal", + "generic_button_rss": "RSS", + "channel_tab_releases_label": "Yayınlar", + "playlist_button_add_items": "Video ekle", + "channel_tab_podcasts_label": "Podcast'ler" } From b7f6c265f74b89ea5079516b1b6d756bc76f2d67 Mon Sep 17 00:00:00 2001 From: maboroshin Date: Mon, 17 Jul 2023 09:06:35 +0000 Subject: [PATCH 331/598] Update Japanese translation --- locales/ja.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/locales/ja.json b/locales/ja.json index b489ece0..ba3641fc 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -460,5 +460,13 @@ "Channel Sponsor": "チャンネルのスポンサー", "Standard YouTube license": "標準 Youtube ライセンス", "Download is disabled": "ダウンロード: このインスタンスでは未対応", - "Import YouTube playlist (.csv)": "YouTube 再生リストをインポート (.csv)" + "Import YouTube playlist (.csv)": "YouTube 再生リストをインポート (.csv)", + "generic_button_delete": "削除", + "generic_button_cancel": "キャンセル", + "channel_tab_podcasts_label": "ポッドキャスト", + "channel_tab_releases_label": "リリース", + "generic_button_edit": "編集", + "generic_button_save": "保存", + "generic_button_rss": "RSS", + "playlist_button_add_items": "動画を追加" } From a337150cbf21e97d848e542053e21ea83166dced Mon Sep 17 00:00:00 2001 From: xrfmkrh Date: Mon, 17 Jul 2023 13:03:36 +0000 Subject: [PATCH 332/598] Update Korean translation --- locales/ko.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/locales/ko.json b/locales/ko.json index 9c8db5a1..e02a8316 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -460,5 +460,13 @@ "Music in this video": "동영상 속 음악", "Artist: ": "아티스트: ", "Download is disabled": "다운로드가 비활성화 되어있음", - "Import YouTube playlist (.csv)": "유튜브 플레이리스트 가져오기 (.csv)" + "Import YouTube playlist (.csv)": "유튜브 플레이리스트 가져오기 (.csv)", + "playlist_button_add_items": "동영상 추가", + "channel_tab_podcasts_label": "팟캐스트", + "generic_button_delete": "삭제", + "generic_button_edit": "편집", + "generic_button_save": "저장", + "generic_button_cancel": "취소", + "generic_button_rss": "RSS", + "channel_tab_releases_label": "출시" } From 979168d8defd0586316f1f0c23f19b3533233f85 Mon Sep 17 00:00:00 2001 From: Nidi Date: Wed, 19 Jul 2023 18:56:49 +0200 Subject: [PATCH 333/598] Add Azerbaijani translation --- locales/az.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 locales/az.json diff --git a/locales/az.json b/locales/az.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/locales/az.json @@ -0,0 +1 @@ +{} From 6d0a6870cb3dc70917680da6625352f59f1e2a68 Mon Sep 17 00:00:00 2001 From: Jeff Huang Date: Thu, 20 Jul 2023 02:34:31 +0000 Subject: [PATCH 334/598] Update Chinese (Traditional) translation --- locales/zh-TW.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 7da2d762..da81922b 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -460,5 +460,13 @@ "Song: ": "歌曲: ", "Standard YouTube license": "標準 YouTube 授權條款", "Download is disabled": "已停用下載", - "Import YouTube playlist (.csv)": "匯入 YouTube 播放清單 (.csv)" + "Import YouTube playlist (.csv)": "匯入 YouTube 播放清單 (.csv)", + "generic_button_cancel": "取消", + "generic_button_edit": "編輯", + "generic_button_save": "儲存", + "generic_button_rss": "RSS", + "generic_button_delete": "刪除", + "playlist_button_add_items": "新增影片", + "channel_tab_podcasts_label": "Podcast", + "channel_tab_releases_label": "發布" } From d83f92a074e60950265c80d6c26ae1949ff17a99 Mon Sep 17 00:00:00 2001 From: VoidWalker Date: Sat, 22 Jul 2023 01:53:24 +0000 Subject: [PATCH 335/598] Update Russian translation --- locales/ru.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/locales/ru.json b/locales/ru.json index a93207ad..5325a9b6 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -492,5 +492,13 @@ "Standard YouTube license": "Стандартная лицензия YouTube", "Channel Sponsor": "Спонсор канала", "Download is disabled": "Загрузка отключена", - "Import YouTube playlist (.csv)": "Импорт плейлиста YouTube (.csv)" + "Import YouTube playlist (.csv)": "Импорт плейлиста YouTube (.csv)", + "channel_tab_releases_label": "Релизы", + "generic_button_delete": "Удалить", + "generic_button_edit": "Редактировать", + "generic_button_save": "Сохранить", + "generic_button_cancel": "Отменить", + "generic_button_rss": "RSS", + "playlist_button_add_items": "Добавить видео", + "channel_tab_podcasts_label": "Подкасты" } From 991d30066d91e72286e536509f3b6863b751f2a9 Mon Sep 17 00:00:00 2001 From: maboroshin Date: Fri, 21 Jul 2023 23:48:40 +0000 Subject: [PATCH 336/598] Update Japanese translation --- locales/ja.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/ja.json b/locales/ja.json index ba3641fc..6fc02e2d 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -81,7 +81,7 @@ "preferences_category_subscription": "登録チャンネル設定", "preferences_annotations_subscribed_label": "最初から登録チャンネルのアノテーションを表示 ", "Redirect homepage to feed: ": "ホームからフィードにリダイレクト: ", - "preferences_max_results_label": "フィードに表示する動画の量: ", + "preferences_max_results_label": "フィードに表示する動画数: ", "preferences_sort_label": "動画を並び替え: ", "published": "投稿日", "published - reverse": "投稿日 - 逆順", From b6b364c7307c162ec06df45055f158999b9d8219 Mon Sep 17 00:00:00 2001 From: joaooliva Date: Thu, 20 Jul 2023 20:39:27 +0000 Subject: [PATCH 337/598] Update Portuguese (Brazil) translation --- locales/pt-BR.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/locales/pt-BR.json b/locales/pt-BR.json index 81290398..68a6e3ab 100644 --- a/locales/pt-BR.json +++ b/locales/pt-BR.json @@ -475,6 +475,14 @@ "Standard YouTube license": "Licença padrão do YouTube", "Song: ": "Música: ", "Channel Sponsor": "Patrocinador do Canal", - "Download is disabled": "Download está desativado", - "Import YouTube playlist (.csv)": "Importar lista de reprodução do YouTube (.csv)" + "Download is disabled": "Download está desabilitado", + "Import YouTube playlist (.csv)": "Importar lista de reprodução do YouTube (.csv)", + "generic_button_delete": "Apagar", + "generic_button_save": "Salvar", + "generic_button_edit": "Editar", + "playlist_button_add_items": "Adicionar vídeos", + "channel_tab_releases_label": "Lançamentos", + "channel_tab_podcasts_label": "Podcasts", + "generic_button_cancel": "Cancelar", + "generic_button_rss": "RSS" } From b41574481df3f6c29967b60ec15eb568ad6b7489 Mon Sep 17 00:00:00 2001 From: Milo Ivir Date: Thu, 20 Jul 2023 12:25:07 +0000 Subject: [PATCH 338/598] Update Croatian translation --- locales/hr.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/locales/hr.json b/locales/hr.json index 0549fa70..ba3dd5e5 100644 --- a/locales/hr.json +++ b/locales/hr.json @@ -492,5 +492,13 @@ "Song: ": "Pjesma: ", "Standard YouTube license": "Standardna YouTube licenca", "Download is disabled": "Preuzimanje je deaktivirano", - "Import YouTube playlist (.csv)": "Uvezi YouTube zbirku (.csv)" + "Import YouTube playlist (.csv)": "Uvezi YouTube zbirku (.csv)", + "generic_button_delete": "Izbriši", + "playlist_button_add_items": "Dodaj videa", + "channel_tab_podcasts_label": "Podcasti", + "generic_button_edit": "Uredi", + "generic_button_save": "Spremi", + "generic_button_cancel": "Odustani", + "generic_button_rss": "RSS", + "channel_tab_releases_label": "Izdanja" } From 7bf3f08daf5854a323a1807024a43cc97f7d280e Mon Sep 17 00:00:00 2001 From: Fjuro Date: Fri, 21 Jul 2023 19:24:09 +0000 Subject: [PATCH 339/598] Update Czech translation --- locales/cs.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/locales/cs.json b/locales/cs.json index 73ed960d..b2cce0bd 100644 --- a/locales/cs.json +++ b/locales/cs.json @@ -492,5 +492,13 @@ "Song: ": "Skladba: ", "Standard YouTube license": "Standardní licence YouTube", "Download is disabled": "Stahování je zakázáno", - "Import YouTube playlist (.csv)": "Importovat YouTube playlist (.csv)" + "Import YouTube playlist (.csv)": "Importovat YouTube playlist (.csv)", + "generic_button_save": "Uložit", + "generic_button_delete": "Odstranit", + "generic_button_cancel": "Zrušit", + "channel_tab_podcasts_label": "Podcasty", + "channel_tab_releases_label": "Vydání", + "generic_button_edit": "Upravit", + "generic_button_rss": "RSS", + "playlist_button_add_items": "Přidat videa" } From 8a88e51382f57bdc4e5b2edd11d569e97eec4321 Mon Sep 17 00:00:00 2001 From: Subham Jena Date: Mon, 24 Jul 2023 14:23:07 +0000 Subject: [PATCH 340/598] Update Odia translation --- locales/or.json | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/locales/or.json b/locales/or.json index 0967ef42..948610f1 100644 --- a/locales/or.json +++ b/locales/or.json @@ -1 +1,29 @@ -{} +{ + "preferences_quality_dash_option_720p": "୭୨୦ପି", + "preferences_quality_dash_option_4320p": "୪୩୨୦ପି", + "preferences_quality_dash_option_240p": "୨୪୦ପି", + "preferences_quality_dash_option_2160p": "୨୧୬୦ପି", + "preferences_quality_dash_option_144p": "୧୪୪ପି", + "reddit": "Reddit", + "preferences_quality_dash_option_480p": "୪୮୦ପି", + "preferences_dark_mode_label": "ଥିମ୍: ", + "dark": "ଗାଢ଼", + "published": "ପ୍ରକାଶିତ", + "generic_videos_count": "{{count}}ଟିଏ ଵିଡ଼ିଓ", + "generic_videos_count_plural": "{{count}}ଟି ଵିଡ଼ିଓ", + "generic_button_edit": "ସମ୍ପାଦନା", + "light": "ହାଲୁକା", + "last": "ଗତ", + "New password": "ନୂଆ ପାସ୍‌ୱର୍ଡ଼", + "preferences_quality_dash_option_1440p": "୧୪୪୦ପି", + "preferences_quality_dash_option_360p": "୩୬୦ପି", + "preferences_quality_option_medium": "ମଧ୍ୟମ", + "preferences_quality_dash_option_1080p": "୧୦୮୦ପି", + "youtube": "YouTube", + "preferences_quality_option_hd720": "HD୭୨୦", + "invidious": "Invidious", + "generic_playlists_count": "{{count}}ଟିଏ ଚାଳନାତାଲିକା", + "generic_playlists_count_plural": "{{count}}ଟି ଚାଳନାତାଲିକା", + "Yes": "ହଁ", + "No": "ନାହିଁ" +} From a5bcf9ba441baaa70d7b4f7ad9abb9211e76dd52 Mon Sep 17 00:00:00 2001 From: Overplant Poster Date: Wed, 26 Jul 2023 21:15:01 +0000 Subject: [PATCH 341/598] Update Sinhala translation --- locales/si.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/locales/si.json b/locales/si.json index 19f34fac..4637cbd2 100644 --- a/locales/si.json +++ b/locales/si.json @@ -89,7 +89,7 @@ "preferences_quality_option_hd720": "HD720", "preferences_quality_dash_option_auto": "ස්වයංක්‍රීය", "preferences_quality_option_small": "කුඩා", - "preferences_quality_dash_option_best": "උසස්", + "preferences_quality_dash_option_best": "හොඳම", "preferences_quality_dash_option_2160p": "2160p", "preferences_quality_dash_option_1440p": "1440p", "preferences_quality_dash_option_720p": "720p", @@ -119,5 +119,9 @@ "Only show latest unwatched video from channel: ": "නාලිකාවේ නවතම නැරඹන නොලද වීඩියෝව පමණක් පෙන්වන්න: ", "preferences_category_data": "දත්ත මනාප", "Clear watch history": "නැරඹුම් ඉතිහාසය මකාදැමීම", - "Subscriptions": "දායකත්ව" + "Subscriptions": "දායකත්ව", + "generic_button_rss": "RSS", + "generic_button_save": "සුරකින්න", + "generic_button_cancel": "අවලංගු කරන්න", + "preferences_quality_dash_option_worst": "නරකම" } From 2117e34e9748a928527b1fda78f6fe883cc5253a Mon Sep 17 00:00:00 2001 From: John Donne Date: Sun, 30 Jul 2023 21:47:27 +0000 Subject: [PATCH 342/598] Update French translation --- locales/fr.json | 90 +++++++++++++++++++++++++++++-------------------- 1 file changed, 54 insertions(+), 36 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index 2eb4dd2b..5e0f5152 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -1,14 +1,19 @@ { - "generic_views_count": "{{count}} vue", - "generic_views_count_plural": "{{count}} vues", - "generic_videos_count": "{{count}} vidéo", - "generic_videos_count_plural": "{{count}} vidéos", - "generic_playlists_count": "{{count}} liste de lecture", - "generic_playlists_count_plural": "{{count}} listes de lecture", - "generic_subscribers_count": "{{count}} abonné", - "generic_subscribers_count_plural": "{{count}} abonnés", - "generic_subscriptions_count": "{{count}} abonnement", - "generic_subscriptions_count_plural": "{{count}} abonnements", + "generic_views_count_0": "{{count}} vue", + "generic_views_count_1": "{{count}} vues", + "generic_views_count_2": "{{count}} vues", + "generic_videos_count_0": "{{count}} vidéo", + "generic_videos_count_1": "{{count}} vidéos", + "generic_videos_count_2": "{{count}} vidéos", + "generic_playlists_count_0": "{{count}} liste de lecture", + "generic_playlists_count_1": "{{count}} listes de lecture", + "generic_playlists_count_2": "{{count}} listes de lecture", + "generic_subscribers_count_0": "{{count}} abonné", + "generic_subscribers_count_1": "{{count}} abonnés", + "generic_subscribers_count_2": "{{count}} abonnés", + "generic_subscriptions_count_0": "{{count}} abonnement", + "generic_subscriptions_count_1": "{{count}} abonnements", + "generic_subscriptions_count_2": "{{count}} abonnements", "generic_button_delete": "Supprimer", "generic_button_edit": "Editer", "generic_button_save": "Enregistrer", @@ -55,10 +60,10 @@ "Password": "Mot de passe", "Time (h:mm:ss):": "Heure (h:mm:ss) :", "Text CAPTCHA": "CAPTCHA textuel", - "Image CAPTCHA": "CAPTCHA graphique", - "Sign In": "Se connecter", + "Image CAPTCHA": "CAPTCHA pictural", + "Sign In": "S'identifier", "Register": "S'inscrire", - "E-mail": "E-mail", + "E-mail": "Courriel", "Preferences": "Préférences", "preferences_category_player": "Préférences du lecteur", "preferences_video_loop_label": "Lire en boucle : ", @@ -128,14 +133,16 @@ "Subscription manager": "Gestionnaire d'abonnement", "Token manager": "Gestionnaire de token", "Token": "Token", - "tokens_count": "{{count}} token", - "tokens_count_plural": "{{count}} tokens", + "tokens_count_0": "{{count}} jeton", + "tokens_count_1": "{{count}} jetons", + "tokens_count_2": "{{count}} jetons", "Import/export": "Importer/Exporter", "unsubscribe": "se désabonner", "revoke": "révoquer", "Subscriptions": "Abonnements", - "subscriptions_unseen_notifs_count": "{{count}} notification non vue", - "subscriptions_unseen_notifs_count_plural": "{{count}} notifications non vues", + "subscriptions_unseen_notifs_count_0": "{{count}} notification non vue", + "subscriptions_unseen_notifs_count_1": "{{count}} notifications non vues", + "subscriptions_unseen_notifs_count_2": "{{count}} notifications non vues", "search": "rechercher", "Log out": "Se déconnecter", "Released under the AGPLv3 on Github.": "Publié sous licence AGPLv3 sur GitHub.", @@ -197,12 +204,14 @@ "This channel does not exist.": "Cette chaine n'existe pas.", "Could not get channel info.": "Impossible de charger les informations de cette chaîne.", "Could not fetch comments": "Impossible de charger les commentaires", - "comments_view_x_replies": "Voir {{count}} réponse", - "comments_view_x_replies_plural": "Voir {{count}} réponses", + "comments_view_x_replies_0": "Voir {{count}} réponse", + "comments_view_x_replies_1": "Voir {{count}} réponses", + "comments_view_x_replies_2": "Voir {{count}} réponses", "`x` ago": "il y a `x`", "Load more": "Voir plus", - "comments_points_count": "{{count}} point", - "comments_points_count_plural": "{{count}} points", + "comments_points_count_0": "{{count}} point", + "comments_points_count_1": "{{count}} points", + "comments_points_count_2": "{{count}} points", "Could not create mix.": "Impossible de charger cette liste de lecture.", "Empty playlist": "La liste de lecture est vide", "Not a playlist.": "La liste de lecture est invalide.", @@ -320,20 +329,27 @@ "Yiddish": "Yiddish", "Yoruba": "Yoruba", "Zulu": "Zoulou", - "generic_count_years": "{{count}} an", - "generic_count_years_plural": "{{count}} ans", - "generic_count_months": "{{count}} mois", - "generic_count_months_plural": "{{count}} mois", - "generic_count_weeks": "{{count}} semaine", - "generic_count_weeks_plural": "{{count}} semaines", - "generic_count_days": "{{count}} jour", - "generic_count_days_plural": "{{count}} jours", - "generic_count_hours": "{{count}} heure", - "generic_count_hours_plural": "{{count}} heures", - "generic_count_minutes": "{{count}} minute", - "generic_count_minutes_plural": "{{count}} minutes", - "generic_count_seconds": "{{count}} seconde", - "generic_count_seconds_plural": "{{count}} secondes", + "generic_count_years_0": "{{count}} an", + "generic_count_years_1": "{{count}} ans", + "generic_count_years_2": "{{count}} ans", + "generic_count_months_0": "{{count}} mois", + "generic_count_months_1": "{{count}} mois", + "generic_count_months_2": "{{count}} mois", + "generic_count_weeks_0": "{{count}} semaine", + "generic_count_weeks_1": "{{count}} semaines", + "generic_count_weeks_2": "{{count}} semaines", + "generic_count_days_0": "{{count}} jour", + "generic_count_days_1": "{{count}} jours", + "generic_count_days_2": "{{count}} jours", + "generic_count_hours_0": "{{count}} heure", + "generic_count_hours_1": "{{count}} heures", + "generic_count_hours_2": "{{count}} heures", + "generic_count_minutes_0": "{{count}} minute", + "generic_count_minutes_1": "{{count}} minutes", + "generic_count_minutes_2": "{{count}} minutes", + "generic_count_seconds_0": "{{count}} seconde", + "generic_count_seconds_1": "{{count}} secondes", + "generic_count_seconds_2": "{{count}} secondes", "Fallback comments: ": "Commentaires alternatifs : ", "Popular": "Populaire", "Search": "Rechercher", @@ -482,5 +498,7 @@ "Music in this video": "Musique dans cette vidéo", "Channel Sponsor": "Soutien de la chaîne", "Download is disabled": "Le téléchargement est désactivé", - "Import YouTube playlist (.csv)": "Importer des listes de lecture de Youtube (.csv)" + "Import YouTube playlist (.csv)": "Importer des listes de lecture de Youtube (.csv)", + "channel_tab_releases_label": "Parutions", + "channel_tab_podcasts_label": "Émissions audio" } From b4e9f173ab002ffad987593cab635638e97ecf99 Mon Sep 17 00:00:00 2001 From: atilluF <110931720+atilluF@users.noreply.github.com> Date: Fri, 28 Jul 2023 12:53:31 +0000 Subject: [PATCH 343/598] Update Italian translation --- locales/it.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/locales/it.json b/locales/it.json index 9d633264..29b7445a 100644 --- a/locales/it.json +++ b/locales/it.json @@ -491,5 +491,13 @@ "Song: ": "Canzone: ", "Standard YouTube license": "Licenza standard di YouTube", "Channel Sponsor": "Sponsor del canale", - "Import YouTube playlist (.csv)": "Importa playlist di YouTube (.csv)" + "Import YouTube playlist (.csv)": "Importa playlist di YouTube (.csv)", + "generic_button_edit": "Modifica", + "generic_button_cancel": "Annulla", + "generic_button_rss": "RSS", + "channel_tab_releases_label": "Pubblicazioni", + "generic_button_delete": "Elimina", + "generic_button_save": "Salva", + "playlist_button_add_items": "Aggiungi video", + "channel_tab_podcasts_label": "Podcast" } From 1e170ef7d08ad01cc241c293a1569a537c7fa84b Mon Sep 17 00:00:00 2001 From: random r Date: Sun, 30 Jul 2023 10:13:57 +0000 Subject: [PATCH 344/598] Update Italian translation --- locales/it.json | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/locales/it.json b/locales/it.json index 29b7445a..f7463ee3 100644 --- a/locales/it.json +++ b/locales/it.json @@ -16,7 +16,7 @@ "View playlist on YouTube": "Vedi playlist su YouTube", "newest": "più recente", "oldest": "più vecchio", - "popular": "Tendenze", + "popular": "popolare", "last": "ultimo", "Next page": "Pagina successiva", "Previous page": "Pagina precedente", @@ -119,8 +119,9 @@ "generic_subscriptions_count_0": "{{count}} iscrizione", "generic_subscriptions_count_1": "{{count}} iscrizioni", "generic_subscriptions_count_2": "{{count}} iscrizioni", - "tokens_count": "{{count}} gettone", - "tokens_count_plural": "{{count}} gettoni", + "tokens_count_0": "{{count}} gettone", + "tokens_count_1": "{{count}} gettoni", + "tokens_count_2": "{{count}} gettoni", "Import/export": "Importa/esporta", "unsubscribe": "disiscriviti", "revoke": "revoca", @@ -482,7 +483,7 @@ "channel_tab_shorts_label": "Short", "channel_tab_playlists_label": "Playlist", "channel_tab_channels_label": "Canali", - "channel_tab_streams_label": "Livestream", + "channel_tab_streams_label": "Trasmissioni in diretta", "channel_tab_community_label": "Comunità", "Music in this video": "Musica in questo video", "Artist: ": "Artista: ", From 9715e96adbf65300f895fc1c30d02c25704d5ea8 Mon Sep 17 00:00:00 2001 From: Eric Date: Sat, 29 Jul 2023 04:00:38 +0000 Subject: [PATCH 345/598] Update Chinese (Simplified) translation --- locales/zh-CN.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 58b834fa..62f45a29 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -460,5 +460,13 @@ "Channel Sponsor": "频道赞助者", "Standard YouTube license": "标准 YouTube 许可证", "Download is disabled": "已禁用下载", - "Import YouTube playlist (.csv)": "导入 YouTube 播放列表(.csv)" + "Import YouTube playlist (.csv)": "导入 YouTube 播放列表(.csv)", + "generic_button_cancel": "取消", + "playlist_button_add_items": "添加视频", + "generic_button_delete": "删除", + "channel_tab_podcasts_label": "播客", + "generic_button_edit": "编辑", + "generic_button_save": "保存", + "generic_button_rss": "RSS", + "channel_tab_releases_label": "公告" } From 00ac29a2ba7640b9ef1cbae5f7147935b49fa885 Mon Sep 17 00:00:00 2001 From: Leonardo Colman Date: Sat, 29 Jul 2023 22:15:27 +0000 Subject: [PATCH 346/598] Update Portuguese (Brazil) translation --- locales/pt-BR.json | 80 +++++++++++++++++++++++++++------------------- 1 file changed, 48 insertions(+), 32 deletions(-) diff --git a/locales/pt-BR.json b/locales/pt-BR.json index 68a6e3ab..7d522ed5 100644 --- a/locales/pt-BR.json +++ b/locales/pt-BR.json @@ -112,8 +112,9 @@ "Subscription manager": "Gerenciador de inscrições", "Token manager": "Gerenciador de tokens", "Token": "Token", - "tokens_count": "{{count}} token", - "tokens_count_plural": "{{count}} tokens", + "tokens_count_0": "{{count}} token", + "tokens_count_1": "{{count}} tokens", + "tokens_count_2": "{{count}} tokens", "Import/export": "Importar/Exportar", "unsubscribe": "cancelar inscrição", "revoke": "revogar", @@ -297,20 +298,27 @@ "Yiddish": "Iídiche", "Yoruba": "Iorubá", "Zulu": "Zulu", - "generic_count_years": "{{count}} ano", - "generic_count_years_plural": "{{count}} anos", - "generic_count_months": "{{count}} mês", - "generic_count_months_plural": "{{count}} meses", - "generic_count_weeks": "{{count}} semana", - "generic_count_weeks_plural": "{{count}} semanas", - "generic_count_days": "{{count}} dia", - "generic_count_days_plural": "{{count}} dias", - "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_count_years_0": "{{count}} ano", + "generic_count_years_1": "{{count}} anos", + "generic_count_years_2": "{{count}} anos", + "generic_count_months_0": "{{count}} mês", + "generic_count_months_1": "{{count}} meses", + "generic_count_months_2": "{{count}} meses", + "generic_count_weeks_0": "{{count}} semana", + "generic_count_weeks_1": "{{count}} semanas", + "generic_count_weeks_2": "{{count}} semanas", + "generic_count_days_0": "{{count}} dia", + "generic_count_days_1": "{{count}} dias", + "generic_count_days_2": "{{count}} dias", + "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", "Fallback comments: ": "Comentários alternativos: ", "Popular": "Populares", "Search": "Procurar", @@ -377,20 +385,27 @@ "preferences_quality_dash_label": "Qualidade de vídeo do painel preferida: ", "preferences_region_label": "País do conteúdo: ", "preferences_quality_dash_option_4320p": "4320p", - "generic_videos_count": "{{count}} vídeo", - "generic_videos_count_plural": "{{count}} vídeos", - "generic_playlists_count": "{{count}} lista de reprodução", - "generic_playlists_count_plural": "{{count}} listas de reprodução", - "generic_subscribers_count": "{{count}} inscrito", - "generic_subscribers_count_plural": "{{count}} inscritos", - "generic_subscriptions_count": "{{count}} inscrição", - "generic_subscriptions_count_plural": "{{count}} inscrições", - "subscriptions_unseen_notifs_count": "{{count}} notificação não vista", - "subscriptions_unseen_notifs_count_plural": "{{count}} notificações não vistas", - "comments_view_x_replies": "Ver {{count}} resposta", - "comments_view_x_replies_plural": "Ver {{count}} respostas", - "comments_points_count": "{{count}} ponto", - "comments_points_count_plural": "{{count}} pontos", + "generic_videos_count_0": "{{count}} vídeo", + "generic_videos_count_1": "{{count}} vídeos", + "generic_videos_count_2": "{{count}} vídeos", + "generic_playlists_count_0": "{{count}} lista de reprodução", + "generic_playlists_count_1": "{{count}} listas de reprodução", + "generic_playlists_count_2": "{{count}} listas de reprodução", + "generic_subscribers_count_0": "{{count}} inscrito", + "generic_subscribers_count_1": "{{count}} inscritos", + "generic_subscribers_count_2": "{{count}} inscritos", + "generic_subscriptions_count_0": "{{count}} inscrição", + "generic_subscriptions_count_1": "{{count}} inscrições", + "generic_subscriptions_count_2": "{{count}} inscrições", + "subscriptions_unseen_notifs_count_0": "{{count}} notificação não vista", + "subscriptions_unseen_notifs_count_1": "{{count}} notificações não vistas", + "subscriptions_unseen_notifs_count_2": "{{count}} notificações não vistas", + "comments_view_x_replies_0": "Ver {{count}} resposta", + "comments_view_x_replies_1": "Ver {{count}} respostas", + "comments_view_x_replies_2": "Ver {{count}} respostas", + "comments_points_count_0": "{{count}} ponto", + "comments_points_count_1": "{{count}} pontos", + "comments_points_count_2": "{{count}} pontos", "crash_page_you_found_a_bug": "Parece que você encontrou um erro no Invidious!", "crash_page_before_reporting": "Antes de reportar um erro, verifique se você:", "preferences_save_player_pos_label": "Salvar a posição de reprodução: ", @@ -400,8 +415,9 @@ "crash_page_search_issue": "procurou por um erro existente no GitHub", "crash_page_report_issue": "Se nenhuma opção acima ajudou, por favor abra um novo problema no Github (preferencialmente em inglês) e inclua o seguinte texto (NÃO traduza):", "crash_page_read_the_faq": "leia as Perguntas frequentes (FAQ)", - "generic_views_count": "{{count}} visualização", - "generic_views_count_plural": "{{count}} visualizações", + "generic_views_count_0": "{{count}} visualização", + "generic_views_count_1": "{{count}} visualizações", + "generic_views_count_2": "{{count}} visualizações", "preferences_quality_option_dash": "DASH (qualidade adaptável)", "preferences_quality_option_hd720": "HD720", "preferences_quality_option_small": "Pequeno", From ebb69ee4fd2f381f004bd13e3ef4bb0f1de3f11a Mon Sep 17 00:00:00 2001 From: Hoang Minh Pham Date: Fri, 28 Jul 2023 16:56:35 +0000 Subject: [PATCH 347/598] Update Vietnamese translation --- locales/vi.json | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/locales/vi.json b/locales/vi.json index d79c684c..9cb87d3e 100644 --- a/locales/vi.json +++ b/locales/vi.json @@ -2,7 +2,7 @@ "generic_videos_count_0": "{{count}} video", "generic_subscribers_count_0": "{{count}} người theo dõi", "LIVE": "TRỰC TIẾP", - "Shared `x` ago": "Đã chia sẻ` x` trước", + "Shared `x` ago": "Đã chia sẻ `x` trước", "Unsubscribe": "Hủy theo dõi", "Subscribe": "Theo dõi", "View channel on YouTube": "Xem kênh trên YouTube", @@ -71,7 +71,7 @@ "Dark mode: ": "Chế độ tối: ", "preferences_dark_mode_label": "Chủ đề: ", "dark": "tối", - "light": "ánh sáng", + "light": "sáng", "preferences_thin_mode_label": "Chế độ mỏng: ", "preferences_category_misc": "Tùy chọn khác", "preferences_automatic_instance_redirect_label": "Tự động chuyển hướng phiên bản (dự phòng về redirect.invidious.io): ", @@ -120,7 +120,7 @@ "View JavaScript license information.": "Xem thông tin giấy phép JavaScript.", "View privacy policy.": "Xem chính sách bảo mật.", "Trending": "Xu hướng", - "Public": "Công cộng", + "Public": "Công khai", "Unlisted": "Không hiển thị", "Private": "Riêng tư", "View all playlists": "Xem tất cả danh sách phát", @@ -182,17 +182,17 @@ "Amharic": "Amharic", "Arabic": "Tiếng Ả Rập", "Armenian": "Tiếng Armenia", - "Azerbaijani": "Azerbaijan", - "Bangla": "Bangla", + "Azerbaijani": "Tiếng Azerbaijan", + "Bangla": "Tiếng Bengal", "Basque": "Tiếng Basque", - "Belarusian": "Người Belarus", + "Belarusian": "Tiếng Belarus", "Bosnian": "Tiếng Bosnia", "Bulgarian": "Tiếng Bungari", "Burmese": "Tiếng Miến Điện", "Catalan": "Tiếng Catalan", "Cebuano": "Cebuano", "Chinese (Simplified)": "Tiếng Trung (Giản thể)", - "Chinese (Traditional)": "Truyền thống Trung Hoa)", + "Chinese (Traditional)": "Tiếng Trung (Phồn thể)", "Corsican": "Corsican", "Croatian": "Tiếng Croatia", "Czech": "Tiếng Séc", @@ -219,22 +219,22 @@ "Igbo": "Igbo", "Indonesian": "Tiếng Indonesia", "Irish": "Tiếng Ailen", - "Italian": "Người Ý", + "Italian": "Tiếng Ý", "Japanese": "Tiếng Nhật", "Javanese": "Tiếng Java", "Kannada": "Tiếng Kannada", "Kazakh": "Tiếng Kazakh", "Khmer": "Tiếng Khmer", - "Korean": "Hàn Quốc", + "Korean": "Tiếng Hàn", "Kurdish": "Tiếng Kurd", - "Kyrgyz": "Kyrgyz", - "Lao": "Lào", - "Latin": "Latin", + "Kyrgyz": "Tiếng Kyrgyz", + "Lao": "Tiếng Lào", + "Latin": "Tiếng Latin", "Latvian": "Tiếng Latvia", "Lithuanian": "Tiếng Litva", "Luxembourgish": "Tiếng Luxembourg", - "Macedonian": "Người Macedonian", - "Malagasy": "Malagasy", + "Macedonian": "Tiếng Macedonian", + "Malagasy": "Tiếng Malagasy", "Malay": "Tiếng Mã Lai", "Malayalam": "Tiếng Malayalam", "Maltese": "Cây nho", @@ -364,7 +364,7 @@ "Import/export": "Xuất/nhập dữ liệu", "preferences_quality_dash_option_4320p": "4320p", "preferences_quality_option_dash": "DASH (tự tối ưu chất lượng)", - "generic_subscriptions_count_0": "{{count}} thuê bao", + "generic_subscriptions_count_0": "{{count}} người đăng kí", "preferences_quality_dash_option_1440p": "1440p", "preferences_quality_dash_option_480p": "480p", "preferences_quality_dash_option_2160p": "2160p", @@ -383,5 +383,9 @@ "Standard YouTube license": "Giấy phép YouTube thông thường", "Album: ": "Album: ", "preferences_save_player_pos_label": "Lưu vị trí xem cuối cùng ", - "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Xin chào! Có vẻ như bạn đã tắt JavaScript. Bấm vào đây để xem bình luận, lưu ý rằng thời gian tải có thể lâu hơn." + "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Xin chào! Có vẻ như bạn đã tắt JavaScript. Bấm vào đây để xem bình luận, lưu ý rằng thời gian tải có thể lâu hơn.", + "Chinese (China)": "Tiếng Trung (Trung Quốc)", + "generic_button_cancel": "Hủy", + "Chinese": "Tiếng Trung", + "generic_button_delete": "Xóa" } From 3123478cb2477969bf49e953c46aaaaeaddfd1bb Mon Sep 17 00:00:00 2001 From: Leonardo Colman Date: Sat, 29 Jul 2023 22:10:14 +0000 Subject: [PATCH 348/598] Update Portuguese translation --- locales/pt.json | 94 +++++++++++++++++++++++++++++++------------------ 1 file changed, 59 insertions(+), 35 deletions(-) diff --git a/locales/pt.json b/locales/pt.json index dfa411c3..df63abe6 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -19,7 +19,7 @@ "search_filters_features_option_hdr": "HDR", "search_filters_features_option_location": "Localização", "search_filters_features_option_four_k": "4K", - "search_filters_features_option_live": "Em direto", + "search_filters_features_option_live": "Ao Vivo", "search_filters_features_option_three_d": "3D", "search_filters_features_option_c_commons": "Creative Commons", "search_filters_features_option_subtitles": "Legendas", @@ -44,20 +44,27 @@ "Default": "Predefinido", "Top": "Destaques", "Search": "Pesquisar", - "generic_count_years": "{{count}} segundo", - "generic_count_years_plural": "{{count}} segundos", - "generic_count_months": "{{count}} minuto", - "generic_count_months_plural": "{{count}} minutos", - "generic_count_weeks": "{{count}} hora", - "generic_count_weeks_plural": "{{count}} horas", - "generic_count_days": "{{count}} dia", - "generic_count_days_plural": "{{count}} dias", - "generic_count_hours": "{{count}} seman", - "generic_count_hours_plural": "{{count}} semanas", - "generic_count_minutes": "{{count}} mês", - "generic_count_minutes_plural": "{{count}} meses", - "generic_count_seconds": "{{count}} ano", - "generic_count_seconds_plural": "{{count}} anos", + "generic_count_years_0": "{{count}} segundo", + "generic_count_years_1": "{{count}} segundos", + "generic_count_years_2": "{{count}} segundos", + "generic_count_months_0": "{{count}} minuto", + "generic_count_months_1": "{{count}} minutos", + "generic_count_months_2": "{{count}} minutos", + "generic_count_weeks_0": "{{count}} hora", + "generic_count_weeks_1": "{{count}} horas", + "generic_count_weeks_2": "{{count}} horas", + "generic_count_days_0": "{{count}} dia", + "generic_count_days_1": "{{count}} dias", + "generic_count_days_2": "{{count}} dias", + "generic_count_hours_0": "{{count}} seman", + "generic_count_hours_1": "{{count}} semanas", + "generic_count_hours_2": "{{count}} semanas", + "generic_count_minutes_0": "{{count}} mês", + "generic_count_minutes_1": "{{count}} meses", + "generic_count_minutes_2": "{{count}} meses", + "generic_count_seconds_0": "{{count}} ano", + "generic_count_seconds_1": "{{count}} anos", + "generic_count_seconds_2": "{{count}} anos", "Chinese (Traditional)": "Chinês (tradicional)", "Chinese (Simplified)": "Chinês (simplificado)", "Could not pull trending pages.": "Não foi possível obter as páginas de tendências.", @@ -167,8 +174,9 @@ "Log out": "Terminar sessão", "Subscriptions": "Subscrições", "revoke": "revogar", - "tokens_count": "{{count}} token", - "tokens_count_plural": "{{count}} tokens", + "tokens_count_0": "{{count}} token", + "tokens_count_1": "{{count}} tokens", + "tokens_count_2": "{{count}} tokens", "Token": "Token", "Token manager": "Gerir tokens", "Subscription manager": "Gerir subscrições", @@ -365,7 +373,7 @@ "Subscribe": "Subscrever", "Unsubscribe": "Anular subscrição", "Shared `x` ago": "Partilhado `x` atrás", - "LIVE": "Em direto", + "LIVE": "AO VIVO", "search_filters_duration_option_short": "Curto (< 4 minutos)", "search_filters_duration_option_long": "Longo (> 20 minutos)", "footer_source_code": "Código-fonte", @@ -402,24 +410,32 @@ "videoinfo_youTube_embed_link": "Incorporar", "preferences_save_player_pos_label": "Guardar a posição de reprodução atual do vídeo: ", "download_subtitles": "Legendas - `x` (.vtt)", - "generic_views_count": "{{count}} visualização", - "generic_views_count_plural": "{{count}} visualizações", + "generic_views_count_0": "{{count}} visualização", + "generic_views_count_1": "{{count}} visualizações", + "generic_views_count_2": "{{count}} visualizações", "videoinfo_started_streaming_x_ago": "Iniciou a transmissão há `x`", "user_saved_playlists": "`x` listas de reprodução guardadas", - "generic_videos_count": "{{count}} vídeo", - "generic_videos_count_plural": "{{count}} vídeos", - "generic_playlists_count": "{{count}} lista de reprodução", - "generic_playlists_count_plural": "{{count}} listas de reprodução", - "subscriptions_unseen_notifs_count": "{{count}} notificação não vista", - "subscriptions_unseen_notifs_count_plural": "{{count}} notificações não vistas", - "comments_view_x_replies": "Ver {{count}} resposta", - "comments_view_x_replies_plural": "Ver {{count}} respostas", - "generic_subscribers_count": "{{count}} inscrito", - "generic_subscribers_count_plural": "{{count}} inscritos", - "generic_subscriptions_count": "{{count}} inscrição", - "generic_subscriptions_count_plural": "{{count}} inscrições", - "comments_points_count": "{{count}} ponto", - "comments_points_count_plural": "{{count}} pontos", + "generic_videos_count_0": "{{count}} vídeo", + "generic_videos_count_1": "{{count}} vídeos", + "generic_videos_count_2": "{{count}} vídeos", + "generic_playlists_count_0": "{{count}} lista de reprodução", + "generic_playlists_count_1": "{{count}} listas de reprodução", + "generic_playlists_count_2": "{{count}} listas de reprodução", + "subscriptions_unseen_notifs_count_0": "{{count}} notificação não vista", + "subscriptions_unseen_notifs_count_1": "{{count}} notificações não vistas", + "subscriptions_unseen_notifs_count_2": "{{count}} notificações não vistas", + "comments_view_x_replies_0": "Ver {{count}} resposta", + "comments_view_x_replies_1": "Ver {{count}} respostas", + "comments_view_x_replies_2": "Ver {{count}} respostas", + "generic_subscribers_count_0": "{{count}} inscrito", + "generic_subscribers_count_1": "{{count}} inscritos", + "generic_subscribers_count_2": "{{count}} inscritos", + "generic_subscriptions_count_0": "{{count}} inscrição", + "generic_subscriptions_count_1": "{{count}} inscrições", + "generic_subscriptions_count_2": "{{count}} inscrições", + "comments_points_count_0": "{{count}} ponto", + "comments_points_count_1": "{{count}} pontos", + "comments_points_count_2": "{{count}} pontos", "crash_page_you_found_a_bug": "Parece que encontrou um erro no Invidious!", "crash_page_before_reporting": "Antes de reportar um erro, verifique se:", "crash_page_refresh": "tentou recarregar a página", @@ -476,5 +492,13 @@ "Channel Sponsor": "Patrocinador do canal", "Standard YouTube license": "Licença padrão do YouTube", "Download is disabled": "A descarga está desativada", - "Import YouTube playlist (.csv)": "Importar lista de reprodução do YouTube (.csv)" + "Import YouTube playlist (.csv)": "Importar lista de reprodução do YouTube (.csv)", + "generic_button_delete": "Deletar", + "generic_button_edit": "Editar", + "generic_button_rss": "RSS", + "channel_tab_podcasts_label": "Podcasts", + "channel_tab_releases_label": "Lançamentos", + "generic_button_save": "Salvar", + "generic_button_cancel": "Cancelar", + "playlist_button_add_items": "Adicionar vídeos" } From 709bb7281b3856421084ea9127c1504b6eb6db96 Mon Sep 17 00:00:00 2001 From: Damjan Gerl Date: Mon, 31 Jul 2023 18:55:04 +0000 Subject: [PATCH 349/598] Update Slovenian translation --- locales/sl.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/locales/sl.json b/locales/sl.json index 45f63c6b..de0c7812 100644 --- a/locales/sl.json +++ b/locales/sl.json @@ -508,5 +508,13 @@ "Standard YouTube license": "Standardna licenca YouTube", "Channel Sponsor": "Sponzor kanala", "Download is disabled": "Prenos je onemogočen", - "Import YouTube playlist (.csv)": "Uvoz seznama predvajanja YouTube (.csv)" + "Import YouTube playlist (.csv)": "Uvoz seznama predvajanja YouTube (.csv)", + "generic_button_delete": "Izbriši", + "generic_button_edit": "Uredi", + "generic_button_save": "Shrani", + "generic_button_cancel": "Prekliči", + "generic_button_rss": "RSS", + "playlist_button_add_items": "Dodaj videoposnetke", + "channel_tab_podcasts_label": "Poddaje", + "channel_tab_releases_label": "Izdaje" } From a81c0f329cfe0ef343c31636b74615e91e613f72 Mon Sep 17 00:00:00 2001 From: syeopite Date: Tue, 8 Aug 2023 15:13:23 -0700 Subject: [PATCH 350/598] Add workaround for storyboards on priv. instances An upstream problem with videojs-vtt-thumbnails means that URLs gets joined incorrectly on any instance where `domain`, `external_port` and `https_only` aren't set. This commit adds some logic with the 404 handler to mitigate this problem. This is however only a workaround. See: https://github.com/iv-org/invidious/issues/3117 https://github.com/chrisboustead/videojs-vtt-thumbnails/issues/31 --- src/invidious/routes/errors.cr | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/invidious/routes/errors.cr b/src/invidious/routes/errors.cr index b138b562..4d8d9ee8 100644 --- a/src/invidious/routes/errors.cr +++ b/src/invidious/routes/errors.cr @@ -1,5 +1,10 @@ module Invidious::Routes::ErrorRoutes def self.error_404(env) + # Workaround for # 3117 + if HOST_URL.empty? && env.request.path.starts_with?("/v1/storyboards/sb") + return env.redirect "#{env.request.path[15..]}?#{env.params.query}" + end + if md = env.request.path.match(/^\/(?([a-zA-Z0-9_-]{11})|(\w+))$/) item = md["id"] From 6b17bb525095a62b163489c565edb0ca29eb1a93 Mon Sep 17 00:00:00 2001 From: syeopite Date: Tue, 8 Aug 2023 15:20:48 -0700 Subject: [PATCH 351/598] Regression from #4037 | Fix storyboards PR #4037 introduced a workaround around YouTube's new integrity checks on streaming URLs. However, the usage of this workaround prevents storyboard data from being returned by InnerTube. This commit fixes that by only using the workaround when calling try_fetch_streaming_data --- src/invidious/videos/parser.cr | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr index 2a09d187..06ff96b1 100644 --- a/src/invidious/videos/parser.cr +++ b/src/invidious/videos/parser.cr @@ -55,9 +55,7 @@ def extract_video_info(video_id : String, proxy_region : String? = nil) client_config = YoutubeAPI::ClientConfig.new(proxy_region: proxy_region) # Fetch data from the player endpoint - # CgIQBg is a workaround for streaming URLs that returns a 403. - # See https://github.com/iv-org/invidious/issues/4027#issuecomment-1666944520 - player_response = YoutubeAPI.player(video_id: video_id, params: "CgIQBg", client_config: client_config) + player_response = YoutubeAPI.player(video_id: video_id, params: "", client_config: client_config) playability_status = player_response.dig?("playabilityStatus", "status").try &.as_s @@ -120,6 +118,9 @@ def extract_video_info(video_id : String, proxy_region : String? = nil) # Replace player response and reset reason if !new_player_response.nil? + # Preserve storyboard data before replacement + new_player_response["storyboards"] = player_response["storyboards"] if player_response["storyboards"]? + player_response = new_player_response params.delete("reason") end From 2b36d3b419d04fd4fc46e97e03a4c3af7285b663 Mon Sep 17 00:00:00 2001 From: syeopite <70992037+syeopite@users.noreply.github.com> Date: Thu, 10 Aug 2023 18:45:10 +0000 Subject: [PATCH 352/598] Update errors.cr --- src/invidious/routes/errors.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/routes/errors.cr b/src/invidious/routes/errors.cr index 4d8d9ee8..1e9ab44e 100644 --- a/src/invidious/routes/errors.cr +++ b/src/invidious/routes/errors.cr @@ -1,6 +1,6 @@ module Invidious::Routes::ErrorRoutes def self.error_404(env) - # Workaround for # 3117 + # Workaround for #3117 if HOST_URL.empty? && env.request.path.starts_with?("/v1/storyboards/sb") return env.redirect "#{env.request.path[15..]}?#{env.params.query}" end From c089d57cdb5517ca199e2ddecc5e54906dc55a8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milan=20=C5=A0alka?= Date: Thu, 10 Aug 2023 10:22:06 +0000 Subject: [PATCH 353/598] Update Slovak translation --- locales/sk.json | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/locales/sk.json b/locales/sk.json index 7346dc58..86681dfa 100644 --- a/locales/sk.json +++ b/locales/sk.json @@ -99,5 +99,23 @@ "generic_subscriptions_count_1": "{{count}} odbery", "generic_subscriptions_count_2": "{{count}} odberov", "Authorize token for `x`?": "Autorizovať token pre `x`?", - "View playlist on YouTube": "Zobraziť playlist na YouTube" + "View playlist on YouTube": "Zobraziť playlist na YouTube", + "preferences_quality_dash_option_best": "Najlepšia", + "preferences_quality_dash_option_worst": "Najhoršia", + "preferences_quality_dash_option_1440p": "1440p", + "preferences_quality_dash_option_720p": "720p", + "preferences_quality_option_hd720": "HD720", + "preferences_quality_dash_label": "Preferovaná video kvalita DASH: ", + "preferences_quality_option_dash": "DASH (adaptívna kvalita)", + "preferences_quality_option_small": "Malá", + "preferences_watch_history_label": "Zapnúť históriu pozerania: ", + "preferences_quality_dash_option_240p": "240p", + "preferences_quality_dash_option_1080p": "1080p", + "preferences_quality_dash_option_480p": "480p", + "preferences_quality_dash_option_auto": "Auto", + "preferences_quality_dash_option_144p": "144p", + "preferences_quality_dash_option_2160p": "2160p", + "invidious": "Invidious", + "preferences_quality_dash_option_4320p": "4320p", + "preferences_quality_dash_option_360p": "360p" } From 37f1a6aacfe2de5f52bd754e650883361e82045e Mon Sep 17 00:00:00 2001 From: Ati Date: Thu, 10 Aug 2023 10:21:34 +0000 Subject: [PATCH 354/598] Update Slovak translation --- locales/sk.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/sk.json b/locales/sk.json index 86681dfa..8add0f57 100644 --- a/locales/sk.json +++ b/locales/sk.json @@ -9,7 +9,7 @@ "last": "posledné", "Next page": "Ďalšia strana", "Previous page": "Predchádzajúca strana", - "Clear watch history?": "Vymazať históriu sledovania?", + "Clear watch history?": "Vymazať históriu pozerania?", "New password": "Nové heslo", "New passwords must match": "Nové heslá sa musia zhodovať", "Authorize token?": "Autorizovať token?", From 4b85890c6ddca8e733e44f1d5599fc7c73564fae Mon Sep 17 00:00:00 2001 From: Noa Laznik Date: Fri, 11 Aug 2023 02:52:09 +0000 Subject: [PATCH 355/598] Update Slovenian translation --- locales/sl.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/sl.json b/locales/sl.json index de0c7812..fec1cb62 100644 --- a/locales/sl.json +++ b/locales/sl.json @@ -222,7 +222,7 @@ "search_filters_date_option_week": "Ta teden", "search_filters_type_label": "Vrsta", "search_filters_type_option_all": "Katerakoli vrsta", - "search_filters_type_option_playlist": "Seznami predvajanja", + "search_filters_type_option_playlist": "Seznam predvajanja", "search_filters_features_option_subtitles": "Podnapisi/CC", "search_filters_features_option_location": "Lokacija", "footer_donate_page": "Prispevaj", From de2ea478540c1237a5559c134df1839c69bda950 Mon Sep 17 00:00:00 2001 From: Petter Reinholdtsen Date: Sun, 13 Aug 2023 11:54:19 +0000 Subject: [PATCH 356/598] =?UTF-8?q?Update=20Norwegian=20Bokm=C3=A5l=20tran?= =?UTF-8?q?slation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/nb-NO.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/locales/nb-NO.json b/locales/nb-NO.json index 1e0e9e77..216b559f 100644 --- a/locales/nb-NO.json +++ b/locales/nb-NO.json @@ -154,7 +154,7 @@ "View YouTube comments": "Vis YouTube-kommentarer", "View more comments on Reddit": "Vis flere kommenterer på Reddit", "View `x` comments": { - "([^.,0-9]|^)1([^.,0-9]|$)": "Vis `x` kommentarer", + "([^.,0-9]|^)1([^.,0-9]|$)": "Vis `x` kommentar", "": "Vis `x` kommentarer" }, "View Reddit comments": "Vis Reddit-kommentarer", @@ -476,5 +476,13 @@ "Album: ": "Album: ", "Download is disabled": "Nedlasting er avskrudd", "Channel Sponsor": "Kanalsponsor", - "Import YouTube playlist (.csv)": "Importer YouTube-spilleliste (.csv)" + "Import YouTube playlist (.csv)": "Importer YouTube-spilleliste (.csv)", + "channel_tab_podcasts_label": "Podkaster", + "channel_tab_releases_label": "Utgaver", + "generic_button_delete": "Slett", + "generic_button_edit": "Endre", + "generic_button_save": "Lagre", + "generic_button_cancel": "Avbryt", + "generic_button_rss": "RSS", + "playlist_button_add_items": "Legg til videoer" } From ce44cb942130d261ee13c37b3ac44025936d4813 Mon Sep 17 00:00:00 2001 From: Snwglb Date: Fri, 18 Aug 2023 08:16:10 +0000 Subject: [PATCH 357/598] Update Hindi translation --- locales/hi.json | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/locales/hi.json b/locales/hi.json index dcb7294d..c1662dd9 100644 --- a/locales/hi.json +++ b/locales/hi.json @@ -471,5 +471,18 @@ "channel_tab_shorts_label": "शॉर्ट्स", "channel_tab_streams_label": "लाइवस्ट्रीम्स", "channel_tab_playlists_label": "प्लेलिस्ट्स", - "channel_tab_channels_label": "चैनल्स" + "channel_tab_channels_label": "चैनल्स", + "generic_button_save": "सहेजें", + "generic_button_cancel": "रद्द करें", + "generic_button_rss": "आरएसएस", + "generic_button_edit": "संपादित करें", + "generic_button_delete": "मिटाएं", + "playlist_button_add_items": "वीडियो जोड़ें", + "Song: ": "गाना: ", + "channel_tab_podcasts_label": "पाॅडकास्ट", + "channel_tab_releases_label": "रिलीज़ेस्", + "Import YouTube playlist (.csv)": "यूट्यूब प्लेलिस्ट को आयात करें", + "Standard YouTube license": "मानक यूट्यूब लाइसेंस", + "Channel Sponsor": "चैनल प्रायोजक", + "Download is disabled": "डाउनलोड करना अक्षम है" } From 387f057a9621ac6a9d6ac2d0f27534ef1f237928 Mon Sep 17 00:00:00 2001 From: Ettore Atalan Date: Sun, 20 Aug 2023 00:48:59 +0000 Subject: [PATCH 358/598] Update German translation --- locales/de.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/locales/de.json b/locales/de.json index 66f2ae6f..6ceaa44b 100644 --- a/locales/de.json +++ b/locales/de.json @@ -476,5 +476,11 @@ "Standard YouTube license": "Standard YouTube-Lizenz", "Song: ": "Musik: ", "Download is disabled": "Herunterladen ist deaktiviert", - "Import YouTube playlist (.csv)": "YouTube Playlist Importieren (.csv)" + "Import YouTube playlist (.csv)": "YouTube Playlist Importieren (.csv)", + "generic_button_delete": "Löschen", + "generic_button_edit": "Bearbeiten", + "generic_button_save": "Speichern", + "generic_button_cancel": "Abbrechen", + "generic_button_rss": "RSS", + "playlist_button_add_items": "Videos hinzufügen" } From 23b19c80b31c1076cecb522a60bd13e1b5b14458 Mon Sep 17 00:00:00 2001 From: Snwglb Date: Sat, 19 Aug 2023 08:45:51 +0000 Subject: [PATCH 359/598] Update Hindi translation --- locales/hi.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/hi.json b/locales/hi.json index c1662dd9..21807c50 100644 --- a/locales/hi.json +++ b/locales/hi.json @@ -481,7 +481,7 @@ "Song: ": "गाना: ", "channel_tab_podcasts_label": "पाॅडकास्ट", "channel_tab_releases_label": "रिलीज़ेस्", - "Import YouTube playlist (.csv)": "यूट्यूब प्लेलिस्ट को आयात करें", + "Import YouTube playlist (.csv)": "YouTube प्लेलिस्ट (.csv) आयात करें", "Standard YouTube license": "मानक यूट्यूब लाइसेंस", "Channel Sponsor": "चैनल प्रायोजक", "Download is disabled": "डाउनलोड करना अक्षम है" From 1f7592e599054131c689246b0dd6aad45f2d8e7a Mon Sep 17 00:00:00 2001 From: syeopite Date: Thu, 24 Aug 2023 16:00:02 -0700 Subject: [PATCH 360/598] Refactor structure of caption.cr Rename CaptionsMetadata to Metadata Nest Metadata under Captions Unnest LANGUAGES constant from Metadata to main Captions module --- src/invidious/frontend/watch_page.cr | 2 +- src/invidious/videos.cr | 6 +- src/invidious/videos/caption.cr | 166 ++++++++++++----------- src/invidious/videos/transcript.cr | 2 +- src/invidious/views/user/preferences.ecr | 2 +- 5 files changed, 90 insertions(+), 88 deletions(-) diff --git a/src/invidious/frontend/watch_page.cr b/src/invidious/frontend/watch_page.cr index b860dba7..5fd81168 100644 --- a/src/invidious/frontend/watch_page.cr +++ b/src/invidious/frontend/watch_page.cr @@ -7,7 +7,7 @@ module Invidious::Frontend::WatchPage getter full_videos : Array(Hash(String, JSON::Any)) getter video_streams : Array(Hash(String, JSON::Any)) getter audio_streams : Array(Hash(String, JSON::Any)) - getter captions : Array(Invidious::Videos::CaptionMetadata) + getter captions : Array(Invidious::Videos::Captions::Metadata) def initialize( @full_videos, diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index 2b1d2603..9fbd1374 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -24,7 +24,7 @@ struct Video property updated : Time @[DB::Field(ignore: true)] - @captions = [] of Invidious::Videos::CaptionMetadata + @captions = [] of Invidious::Videos::Captions::Metadata @[DB::Field(ignore: true)] property adaptive_fmts : Array(Hash(String, JSON::Any))? @@ -215,9 +215,9 @@ struct Video keywords.includes? "YouTube Red" end - def captions : Array(Invidious::Videos::CaptionMetadata) + def captions : Array(Invidious::Videos::Captions::Metadata) if @captions.empty? && @info.has_key?("captions") - @captions = Invidious::Videos::CaptionMetadata.from_yt_json(info["captions"]) + @captions = Invidious::Videos::Captions::Metadata.from_yt_json(info["captions"]) end return @captions diff --git a/src/invidious/videos/caption.cr b/src/invidious/videos/caption.cr index 1e2abde9..82b68dcd 100644 --- a/src/invidious/videos/caption.cr +++ b/src/invidious/videos/caption.cr @@ -1,107 +1,109 @@ require "json" module Invidious::Videos - struct CaptionMetadata - property name : String - property language_code : String - property base_url : String + module Captions + struct Metadata + property name : String + property language_code : String + property base_url : String - property auto_generated : Bool + property auto_generated : Bool - def initialize(@name, @language_code, @base_url, @auto_generated) - end - - # Parse the JSON structure from Youtube - def self.from_yt_json(container : JSON::Any) : Array(CaptionMetadata) - caption_tracks = container - .dig?("playerCaptionsTracklistRenderer", "captionTracks") - .try &.as_a - - captions_list = [] of CaptionMetadata - return captions_list if caption_tracks.nil? - - caption_tracks.each do |caption| - name = caption["name"]["simpleText"]? || caption["name"]["runs"][0]["text"] - name = name.to_s.split(" - ")[0] - - language_code = caption["languageCode"].to_s - base_url = caption["baseUrl"].to_s - - auto_generated = false - if caption["kind"]? && caption["kind"] == "asr" - auto_generated = true - end - - captions_list << CaptionMetadata.new(name, language_code, base_url, auto_generated) + def initialize(@name, @language_code, @base_url, @auto_generated) end - return captions_list - end + # Parse the JSON structure from Youtube + def self.from_yt_json(container : JSON::Any) : Array(Captions::Metadata) + caption_tracks = container + .dig?("playerCaptionsTracklistRenderer", "captionTracks") + .try &.as_a - def timedtext_to_vtt(timedtext : String, tlang = nil) : String - # In the future, we could just directly work with the url. This is more of a POC - cues = [] of XML::Node - tree = XML.parse(timedtext) - tree = tree.children.first + captions_list = [] of Captions::Metadata + return captions_list if caption_tracks.nil? - tree.children.each do |item| - if item.name == "body" - item.children.each do |cue| - if cue.name == "p" && !(cue.children.size == 1 && cue.children[0].content == "\n") - cues << cue + caption_tracks.each do |caption| + name = caption["name"]["simpleText"]? || caption["name"]["runs"][0]["text"] + name = name.to_s.split(" - ")[0] + + language_code = caption["languageCode"].to_s + base_url = caption["baseUrl"].to_s + + auto_generated = false + if caption["kind"]? && caption["kind"] == "asr" + auto_generated = true + end + + captions_list << Captions::Metadata.new(name, language_code, base_url, auto_generated) + end + + return captions_list + end + + def timedtext_to_vtt(timedtext : String, tlang = nil) : String + # In the future, we could just directly work with the url. This is more of a POC + cues = [] of XML::Node + tree = XML.parse(timedtext) + tree = tree.children.first + + tree.children.each do |item| + if item.name == "body" + item.children.each do |cue| + if cue.name == "p" && !(cue.children.size == 1 && cue.children[0].content == "\n") + cues << cue + end end + break end - break end - end - result = String.build do |result| - result << <<-END_VTT - WEBVTT - Kind: captions - Language: #{tlang || @language_code} + result = String.build do |result| + result << <<-END_VTT + WEBVTT + Kind: captions + Language: #{tlang || @language_code} - END_VTT + END_VTT - result << "\n\n" + result << "\n\n" - cues.each_with_index do |node, i| - start_time = node["t"].to_f.milliseconds + cues.each_with_index do |node, i| + start_time = node["t"].to_f.milliseconds - duration = node["d"]?.try &.to_f.milliseconds + duration = node["d"]?.try &.to_f.milliseconds - duration ||= start_time + duration ||= start_time - if cues.size > i + 1 - end_time = cues[i + 1]["t"].to_f.milliseconds - else - end_time = start_time + duration + if cues.size > i + 1 + end_time = cues[i + 1]["t"].to_f.milliseconds + else + end_time = start_time + duration + end + + # start_time + result << start_time.hours.to_s.rjust(2, '0') + result << ':' << start_time.minutes.to_s.rjust(2, '0') + result << ':' << start_time.seconds.to_s.rjust(2, '0') + result << '.' << start_time.milliseconds.to_s.rjust(3, '0') + + result << " --> " + + # end_time + result << end_time.hours.to_s.rjust(2, '0') + result << ':' << end_time.minutes.to_s.rjust(2, '0') + result << ':' << end_time.seconds.to_s.rjust(2, '0') + result << '.' << end_time.milliseconds.to_s.rjust(3, '0') + + result << "\n" + + node.children.each do |s| + result << s.content + end + result << "\n" + result << "\n" end - - # start_time - result << start_time.hours.to_s.rjust(2, '0') - result << ':' << start_time.minutes.to_s.rjust(2, '0') - result << ':' << start_time.seconds.to_s.rjust(2, '0') - result << '.' << start_time.milliseconds.to_s.rjust(3, '0') - - result << " --> " - - # end_time - result << end_time.hours.to_s.rjust(2, '0') - result << ':' << end_time.minutes.to_s.rjust(2, '0') - result << ':' << end_time.seconds.to_s.rjust(2, '0') - result << '.' << end_time.milliseconds.to_s.rjust(3, '0') - - result << "\n" - - node.children.each do |s| - result << s.content - end - result << "\n" - result << "\n" end + return result end - return result end # List of all caption languages available on Youtube. diff --git a/src/invidious/videos/transcript.cr b/src/invidious/videos/transcript.cr index ba2728cd..c86b3988 100644 --- a/src/invidious/videos/transcript.cr +++ b/src/invidious/videos/transcript.cr @@ -37,7 +37,7 @@ module Invidious::Videos # Convert into array of TranscriptLine lines = self.parse(initial_data) - # Taken from Invidious::Videos::CaptionMetadata.timedtext_to_vtt() + # Taken from Invidious::Videos::Captions::Metadata.timedtext_to_vtt() vtt = String.build do |vtt| vtt << <<-END_VTT WEBVTT diff --git a/src/invidious/views/user/preferences.ecr b/src/invidious/views/user/preferences.ecr index b1061ee8..55349c5a 100644 --- a/src/invidious/views/user/preferences.ecr +++ b/src/invidious/views/user/preferences.ecr @@ -89,7 +89,7 @@ <% preferences.captions.each_with_index do |caption, index| %> From 7d435f082bf24c1122c95ecc92efee4a39a7b539 Mon Sep 17 00:00:00 2001 From: syeopite <70992037+syeopite@users.noreply.github.com> Date: Thu, 24 Aug 2023 23:20:20 +0000 Subject: [PATCH 361/598] Update src/invidious/videos/transcript.cr Co-authored-by: Samantaz Fox --- src/invidious/videos/transcript.cr | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/invidious/videos/transcript.cr b/src/invidious/videos/transcript.cr index c86b3988..f3360a52 100644 --- a/src/invidious/videos/transcript.cr +++ b/src/invidious/videos/transcript.cr @@ -4,16 +4,13 @@ module Invidious::Videos record TranscriptLine, start_ms : Time::Span, end_ms : Time::Span, line : String def self.generate_param(video_id : String, language_code : String, auto_generated : Bool) : String - if !auto_generated - is_auto_generated = "" - elsif is_auto_generated = "asr" - end + kind = auto_generated ? "asr" : "" object = { "1:0:string" => video_id, "2:base64" => { - "1:string" => is_auto_generated, + "1:string" => kind, "2:string" => language_code, "3:string" => "", }, From 3615bb0e62209cfad4825e8c40d8e6de69aac687 Mon Sep 17 00:00:00 2001 From: syeopite Date: Thu, 24 Aug 2023 16:21:05 -0700 Subject: [PATCH 362/598] Update src/invidious/videos/caption.cr Co-authored-by: Samantaz Fox --- src/invidious/videos/caption.cr | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/invidious/videos/caption.cr b/src/invidious/videos/caption.cr index 82b68dcd..256dfcc0 100644 --- a/src/invidious/videos/caption.cr +++ b/src/invidious/videos/caption.cr @@ -28,10 +28,7 @@ module Invidious::Videos language_code = caption["languageCode"].to_s base_url = caption["baseUrl"].to_s - auto_generated = false - if caption["kind"]? && caption["kind"] == "asr" - auto_generated = true - end + auto_generated = (caption["kind"]? == "asr") captions_list << Captions::Metadata.new(name, language_code, base_url, auto_generated) end From 1377f2ce7d0a8fed716e8e285902bfbfef1a17e0 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Fri, 25 Aug 2023 08:24:25 +0200 Subject: [PATCH 363/598] Revert broken i18next v3 changes made by weblate --- locales/fr.json | 80 +++++++++++++++++++--------------------------- locales/it.json | 80 +++++++++++++++++++--------------------------- locales/pt-BR.json | 80 +++++++++++++++++++--------------------------- locales/pt.json | 80 +++++++++++++++++++--------------------------- 4 files changed, 128 insertions(+), 192 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index 5e0f5152..286ae361 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -1,19 +1,14 @@ { - "generic_views_count_0": "{{count}} vue", - "generic_views_count_1": "{{count}} vues", - "generic_views_count_2": "{{count}} vues", - "generic_videos_count_0": "{{count}} vidéo", - "generic_videos_count_1": "{{count}} vidéos", - "generic_videos_count_2": "{{count}} vidéos", - "generic_playlists_count_0": "{{count}} liste de lecture", - "generic_playlists_count_1": "{{count}} listes de lecture", - "generic_playlists_count_2": "{{count}} listes de lecture", - "generic_subscribers_count_0": "{{count}} abonné", - "generic_subscribers_count_1": "{{count}} abonnés", - "generic_subscribers_count_2": "{{count}} abonnés", - "generic_subscriptions_count_0": "{{count}} abonnement", - "generic_subscriptions_count_1": "{{count}} abonnements", - "generic_subscriptions_count_2": "{{count}} abonnements", + "generic_views_count": "{{count}} vue", + "generic_views_count_plural": "{{count}} vues", + "generic_videos_count": "{{count}} vidéo", + "generic_videos_count_plural": "{{count}} vidéos", + "generic_playlists_count": "{{count}} liste de lecture", + "generic_playlists_count_plural": "{{count}} listes de lecture", + "generic_subscribers_count": "{{count}} abonné", + "generic_subscribers_count_plural": "{{count}} abonnés", + "generic_subscriptions_count": "{{count}} abonnement", + "generic_subscriptions_count_plural": "{{count}} abonnements", "generic_button_delete": "Supprimer", "generic_button_edit": "Editer", "generic_button_save": "Enregistrer", @@ -133,16 +128,14 @@ "Subscription manager": "Gestionnaire d'abonnement", "Token manager": "Gestionnaire de token", "Token": "Token", - "tokens_count_0": "{{count}} jeton", - "tokens_count_1": "{{count}} jetons", - "tokens_count_2": "{{count}} jetons", + "tokens_count": "{{count}} jeton", + "tokens_count_plural": "{{count}} jetons", "Import/export": "Importer/Exporter", "unsubscribe": "se désabonner", "revoke": "révoquer", "Subscriptions": "Abonnements", - "subscriptions_unseen_notifs_count_0": "{{count}} notification non vue", - "subscriptions_unseen_notifs_count_1": "{{count}} notifications non vues", - "subscriptions_unseen_notifs_count_2": "{{count}} notifications non vues", + "subscriptions_unseen_notifs_count": "{{count}} notification non vue", + "subscriptions_unseen_notifs_count_plural": "{{count}} notifications non vues", "search": "rechercher", "Log out": "Se déconnecter", "Released under the AGPLv3 on Github.": "Publié sous licence AGPLv3 sur GitHub.", @@ -204,14 +197,12 @@ "This channel does not exist.": "Cette chaine n'existe pas.", "Could not get channel info.": "Impossible de charger les informations de cette chaîne.", "Could not fetch comments": "Impossible de charger les commentaires", - "comments_view_x_replies_0": "Voir {{count}} réponse", - "comments_view_x_replies_1": "Voir {{count}} réponses", - "comments_view_x_replies_2": "Voir {{count}} réponses", + "comments_view_x_replies": "Voir {{count}} réponse", + "comments_view_x_replies_plural": "Voir {{count}} réponses", "`x` ago": "il y a `x`", "Load more": "Voir plus", - "comments_points_count_0": "{{count}} point", - "comments_points_count_1": "{{count}} points", - "comments_points_count_2": "{{count}} points", + "comments_points_count": "{{count}} point", + "comments_points_count_plural": "{{count}} points", "Could not create mix.": "Impossible de charger cette liste de lecture.", "Empty playlist": "La liste de lecture est vide", "Not a playlist.": "La liste de lecture est invalide.", @@ -329,27 +320,20 @@ "Yiddish": "Yiddish", "Yoruba": "Yoruba", "Zulu": "Zoulou", - "generic_count_years_0": "{{count}} an", - "generic_count_years_1": "{{count}} ans", - "generic_count_years_2": "{{count}} ans", - "generic_count_months_0": "{{count}} mois", - "generic_count_months_1": "{{count}} mois", - "generic_count_months_2": "{{count}} mois", - "generic_count_weeks_0": "{{count}} semaine", - "generic_count_weeks_1": "{{count}} semaines", - "generic_count_weeks_2": "{{count}} semaines", - "generic_count_days_0": "{{count}} jour", - "generic_count_days_1": "{{count}} jours", - "generic_count_days_2": "{{count}} jours", - "generic_count_hours_0": "{{count}} heure", - "generic_count_hours_1": "{{count}} heures", - "generic_count_hours_2": "{{count}} heures", - "generic_count_minutes_0": "{{count}} minute", - "generic_count_minutes_1": "{{count}} minutes", - "generic_count_minutes_2": "{{count}} minutes", - "generic_count_seconds_0": "{{count}} seconde", - "generic_count_seconds_1": "{{count}} secondes", - "generic_count_seconds_2": "{{count}} secondes", + "generic_count_years": "{{count}} an", + "generic_count_years_plural": "{{count}} ans", + "generic_count_months": "{{count}} mois", + "generic_count_months_plural": "{{count}} mois", + "generic_count_weeks": "{{count}} semaine", + "generic_count_weeks_plural": "{{count}} semaines", + "generic_count_days": "{{count}} jour", + "generic_count_days_plural": "{{count}} jours", + "generic_count_hours": "{{count}} heure", + "generic_count_hours_plural": "{{count}} heures", + "generic_count_minutes": "{{count}} minute", + "generic_count_minutes_plural": "{{count}} minutes", + "generic_count_seconds": "{{count}} seconde", + "generic_count_seconds_plural": "{{count}} secondes", "Fallback comments: ": "Commentaires alternatifs : ", "Popular": "Populaire", "Search": "Rechercher", diff --git a/locales/it.json b/locales/it.json index f7463ee3..894eb97f 100644 --- a/locales/it.json +++ b/locales/it.json @@ -1,13 +1,10 @@ { - "generic_subscribers_count_0": "{{count}} iscritto", - "generic_subscribers_count_1": "{{count}} iscritti", - "generic_subscribers_count_2": "{{count}} iscritti", - "generic_videos_count_0": "{{count}} video", - "generic_videos_count_1": "{{count}} video", - "generic_videos_count_2": "{{count}} video", - "generic_playlists_count_0": "{{count}} playlist", - "generic_playlists_count_1": "{{count}} playlist", - "generic_playlists_count_2": "{{count}} playlist", + "generic_subscribers_count": "{{count}} iscritto", + "generic_subscribers_count_plural": "{{count}} iscritti", + "generic_videos_count": "{{count}} video", + "generic_videos_count_plural": "{{count}} video", + "generic_playlists_count": "{{count}} playlist", + "generic_playlists_count_plural": "{{count}} playlist", "LIVE": "IN DIRETTA", "Shared `x` ago": "Condiviso `x` fa", "Unsubscribe": "Disiscriviti", @@ -116,19 +113,16 @@ "Subscription manager": "Gestione delle iscrizioni", "Token manager": "Gestione dei gettoni", "Token": "Gettone", - "generic_subscriptions_count_0": "{{count}} iscrizione", - "generic_subscriptions_count_1": "{{count}} iscrizioni", - "generic_subscriptions_count_2": "{{count}} iscrizioni", - "tokens_count_0": "{{count}} gettone", - "tokens_count_1": "{{count}} gettoni", - "tokens_count_2": "{{count}} gettoni", + "generic_subscriptions_count": "{{count}} iscrizione", + "generic_subscriptions_count_plural": "{{count}} iscrizioni", + "tokens_count": "{{count}} gettone", + "tokens_count_plural": "{{count}} gettoni", "Import/export": "Importa/esporta", "unsubscribe": "disiscriviti", "revoke": "revoca", "Subscriptions": "Iscrizioni", - "subscriptions_unseen_notifs_count_0": "{{count}} notifica non visualizzata", - "subscriptions_unseen_notifs_count_1": "{{count}} notifiche non visualizzate", - "subscriptions_unseen_notifs_count_2": "{{count}} notifiche non visualizzate", + "subscriptions_unseen_notifs_count": "{{count}} notifica non visualizzata", + "subscriptions_unseen_notifs_count_plural": "{{count}} notifiche non visualizzate", "search": "Cerca", "Log out": "Esci", "Source available here.": "Codice sorgente.", @@ -157,9 +151,8 @@ "Whitelisted regions: ": "Regioni in lista bianca: ", "Blacklisted regions: ": "Regioni in lista nera: ", "Shared `x`": "Condiviso `x`", - "generic_views_count_0": "{{count}} visualizzazione", - "generic_views_count_1": "{{count}} visualizzazioni", - "generic_views_count_2": "{{count}} visualizzazioni", + "generic_views_count": "{{count}} visualizzazione", + "generic_views_count_plural": "{{count}} visualizzazioni", "Premieres in `x`": "In anteprima in `x`", "Premieres `x`": "In anteprima `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.": "Ciao, Sembra che tu abbia disattivato JavaScript. Clicca qui per visualizzare i commenti, ma considera che il caricamento potrebbe richiedere più tempo.", @@ -307,27 +300,20 @@ "Yiddish": "Yiddish", "Yoruba": "Yoruba", "Zulu": "Zulu", - "generic_count_years_0": "{{count}} anno", - "generic_count_years_1": "{{count}} anni", - "generic_count_years_2": "{{count}} anni", - "generic_count_months_0": "{{count}} mese", - "generic_count_months_1": "{{count}} mesi", - "generic_count_months_2": "{{count}} mesi", - "generic_count_weeks_0": "{{count}} settimana", - "generic_count_weeks_1": "{{count}} settimane", - "generic_count_weeks_2": "{{count}} settimane", - "generic_count_days_0": "{{count}} giorno", - "generic_count_days_1": "{{count}} giorni", - "generic_count_days_2": "{{count}} giorni", - "generic_count_hours_0": "{{count}} ora", - "generic_count_hours_1": "{{count}} ore", - "generic_count_hours_2": "{{count}} ore", - "generic_count_minutes_0": "{{count}} minuto", - "generic_count_minutes_1": "{{count}} minuti", - "generic_count_minutes_2": "{{count}} minuti", - "generic_count_seconds_0": "{{count}} secondo", - "generic_count_seconds_1": "{{count}} secondi", - "generic_count_seconds_2": "{{count}} secondi", + "generic_count_years": "{{count}} anno", + "generic_count_years_plural": "{{count}} anni", + "generic_count_months": "{{count}} mese", + "generic_count_months_plural": "{{count}} mesi", + "generic_count_weeks": "{{count}} settimana", + "generic_count_weeks_plural": "{{count}} settimane", + "generic_count_days": "{{count}} giorno", + "generic_count_days_plural": "{{count}} giorni", + "generic_count_hours": "{{count}} ora", + "generic_count_hours_plural": "{{count}} ore", + "generic_count_minutes": "{{count}} minuto", + "generic_count_minutes_plural": "{{count}} minuti", + "generic_count_seconds": "{{count}} secondo", + "generic_count_seconds_plural": "{{count}} secondi", "Fallback comments: ": "Commenti alternativi: ", "Popular": "Popolare", "Search": "Cerca", @@ -431,12 +417,10 @@ "search_filters_duration_option_short": "Corto (< 4 minuti)", "search_filters_duration_option_long": "Lungo (> 20 minuti)", "search_filters_features_option_purchased": "Acquistato", - "comments_view_x_replies_0": "Vedi {{count}} risposta", - "comments_view_x_replies_1": "Vedi {{count}} risposte", - "comments_view_x_replies_2": "Vedi {{count}} risposte", - "comments_points_count_0": "{{count}} punto", - "comments_points_count_1": "{{count}} punti", - "comments_points_count_2": "{{count}} punti", + "comments_view_x_replies": "Vedi {{count}} risposta", + "comments_view_x_replies_plural": "Vedi {{count}} risposte", + "comments_points_count": "{{count}} punto", + "comments_points_count_plural": "{{count}} punti", "Portuguese (auto-generated)": "Portoghese (generati automaticamente)", "crash_page_you_found_a_bug": "Sembra che tu abbia trovato un bug in Invidious!", "crash_page_switch_instance": "provato a usare un'altra istanza", diff --git a/locales/pt-BR.json b/locales/pt-BR.json index 7d522ed5..68a6e3ab 100644 --- a/locales/pt-BR.json +++ b/locales/pt-BR.json @@ -112,9 +112,8 @@ "Subscription manager": "Gerenciador de inscrições", "Token manager": "Gerenciador de tokens", "Token": "Token", - "tokens_count_0": "{{count}} token", - "tokens_count_1": "{{count}} tokens", - "tokens_count_2": "{{count}} tokens", + "tokens_count": "{{count}} token", + "tokens_count_plural": "{{count}} tokens", "Import/export": "Importar/Exportar", "unsubscribe": "cancelar inscrição", "revoke": "revogar", @@ -298,27 +297,20 @@ "Yiddish": "Iídiche", "Yoruba": "Iorubá", "Zulu": "Zulu", - "generic_count_years_0": "{{count}} ano", - "generic_count_years_1": "{{count}} anos", - "generic_count_years_2": "{{count}} anos", - "generic_count_months_0": "{{count}} mês", - "generic_count_months_1": "{{count}} meses", - "generic_count_months_2": "{{count}} meses", - "generic_count_weeks_0": "{{count}} semana", - "generic_count_weeks_1": "{{count}} semanas", - "generic_count_weeks_2": "{{count}} semanas", - "generic_count_days_0": "{{count}} dia", - "generic_count_days_1": "{{count}} dias", - "generic_count_days_2": "{{count}} dias", - "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_count_years": "{{count}} ano", + "generic_count_years_plural": "{{count}} anos", + "generic_count_months": "{{count}} mês", + "generic_count_months_plural": "{{count}} meses", + "generic_count_weeks": "{{count}} semana", + "generic_count_weeks_plural": "{{count}} semanas", + "generic_count_days": "{{count}} dia", + "generic_count_days_plural": "{{count}} dias", + "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", "Fallback comments: ": "Comentários alternativos: ", "Popular": "Populares", "Search": "Procurar", @@ -385,27 +377,20 @@ "preferences_quality_dash_label": "Qualidade de vídeo do painel preferida: ", "preferences_region_label": "País do conteúdo: ", "preferences_quality_dash_option_4320p": "4320p", - "generic_videos_count_0": "{{count}} vídeo", - "generic_videos_count_1": "{{count}} vídeos", - "generic_videos_count_2": "{{count}} vídeos", - "generic_playlists_count_0": "{{count}} lista de reprodução", - "generic_playlists_count_1": "{{count}} listas de reprodução", - "generic_playlists_count_2": "{{count}} listas de reprodução", - "generic_subscribers_count_0": "{{count}} inscrito", - "generic_subscribers_count_1": "{{count}} inscritos", - "generic_subscribers_count_2": "{{count}} inscritos", - "generic_subscriptions_count_0": "{{count}} inscrição", - "generic_subscriptions_count_1": "{{count}} inscrições", - "generic_subscriptions_count_2": "{{count}} inscrições", - "subscriptions_unseen_notifs_count_0": "{{count}} notificação não vista", - "subscriptions_unseen_notifs_count_1": "{{count}} notificações não vistas", - "subscriptions_unseen_notifs_count_2": "{{count}} notificações não vistas", - "comments_view_x_replies_0": "Ver {{count}} resposta", - "comments_view_x_replies_1": "Ver {{count}} respostas", - "comments_view_x_replies_2": "Ver {{count}} respostas", - "comments_points_count_0": "{{count}} ponto", - "comments_points_count_1": "{{count}} pontos", - "comments_points_count_2": "{{count}} pontos", + "generic_videos_count": "{{count}} vídeo", + "generic_videos_count_plural": "{{count}} vídeos", + "generic_playlists_count": "{{count}} lista de reprodução", + "generic_playlists_count_plural": "{{count}} listas de reprodução", + "generic_subscribers_count": "{{count}} inscrito", + "generic_subscribers_count_plural": "{{count}} inscritos", + "generic_subscriptions_count": "{{count}} inscrição", + "generic_subscriptions_count_plural": "{{count}} inscrições", + "subscriptions_unseen_notifs_count": "{{count}} notificação não vista", + "subscriptions_unseen_notifs_count_plural": "{{count}} notificações não vistas", + "comments_view_x_replies": "Ver {{count}} resposta", + "comments_view_x_replies_plural": "Ver {{count}} respostas", + "comments_points_count": "{{count}} ponto", + "comments_points_count_plural": "{{count}} pontos", "crash_page_you_found_a_bug": "Parece que você encontrou um erro no Invidious!", "crash_page_before_reporting": "Antes de reportar um erro, verifique se você:", "preferences_save_player_pos_label": "Salvar a posição de reprodução: ", @@ -415,9 +400,8 @@ "crash_page_search_issue": "procurou por um erro existente no GitHub", "crash_page_report_issue": "Se nenhuma opção acima ajudou, por favor abra um novo problema no Github (preferencialmente em inglês) e inclua o seguinte texto (NÃO traduza):", "crash_page_read_the_faq": "leia as Perguntas frequentes (FAQ)", - "generic_views_count_0": "{{count}} visualização", - "generic_views_count_1": "{{count}} visualizações", - "generic_views_count_2": "{{count}} visualizações", + "generic_views_count": "{{count}} visualização", + "generic_views_count_plural": "{{count}} visualizações", "preferences_quality_option_dash": "DASH (qualidade adaptável)", "preferences_quality_option_hd720": "HD720", "preferences_quality_option_small": "Pequeno", diff --git a/locales/pt.json b/locales/pt.json index df63abe6..e7cc4810 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -44,27 +44,20 @@ "Default": "Predefinido", "Top": "Destaques", "Search": "Pesquisar", - "generic_count_years_0": "{{count}} segundo", - "generic_count_years_1": "{{count}} segundos", - "generic_count_years_2": "{{count}} segundos", - "generic_count_months_0": "{{count}} minuto", - "generic_count_months_1": "{{count}} minutos", - "generic_count_months_2": "{{count}} minutos", - "generic_count_weeks_0": "{{count}} hora", - "generic_count_weeks_1": "{{count}} horas", - "generic_count_weeks_2": "{{count}} horas", - "generic_count_days_0": "{{count}} dia", - "generic_count_days_1": "{{count}} dias", - "generic_count_days_2": "{{count}} dias", - "generic_count_hours_0": "{{count}} seman", - "generic_count_hours_1": "{{count}} semanas", - "generic_count_hours_2": "{{count}} semanas", - "generic_count_minutes_0": "{{count}} mês", - "generic_count_minutes_1": "{{count}} meses", - "generic_count_minutes_2": "{{count}} meses", - "generic_count_seconds_0": "{{count}} ano", - "generic_count_seconds_1": "{{count}} anos", - "generic_count_seconds_2": "{{count}} anos", + "generic_count_years": "{{count}} segundo", + "generic_count_years_plural": "{{count}} segundos", + "generic_count_months": "{{count}} minuto", + "generic_count_months_plural": "{{count}} minutos", + "generic_count_weeks": "{{count}} hora", + "generic_count_weeks_plural": "{{count}} horas", + "generic_count_days": "{{count}} dia", + "generic_count_days_plural": "{{count}} dias", + "generic_count_hours": "{{count}} seman", + "generic_count_hours_plural": "{{count}} semanas", + "generic_count_minutes": "{{count}} mês", + "generic_count_minutes_plural": "{{count}} meses", + "generic_count_seconds": "{{count}} ano", + "generic_count_seconds_plural": "{{count}} anos", "Chinese (Traditional)": "Chinês (tradicional)", "Chinese (Simplified)": "Chinês (simplificado)", "Could not pull trending pages.": "Não foi possível obter as páginas de tendências.", @@ -174,9 +167,8 @@ "Log out": "Terminar sessão", "Subscriptions": "Subscrições", "revoke": "revogar", - "tokens_count_0": "{{count}} token", - "tokens_count_1": "{{count}} tokens", - "tokens_count_2": "{{count}} tokens", + "tokens_count": "{{count}} token", + "tokens_count_plural": "{{count}} tokens", "Token": "Token", "Token manager": "Gerir tokens", "Subscription manager": "Gerir subscrições", @@ -410,32 +402,24 @@ "videoinfo_youTube_embed_link": "Incorporar", "preferences_save_player_pos_label": "Guardar a posição de reprodução atual do vídeo: ", "download_subtitles": "Legendas - `x` (.vtt)", - "generic_views_count_0": "{{count}} visualização", - "generic_views_count_1": "{{count}} visualizações", - "generic_views_count_2": "{{count}} visualizações", + "generic_views_count": "{{count}} visualização", + "generic_views_count_plural": "{{count}} visualizações", "videoinfo_started_streaming_x_ago": "Iniciou a transmissão há `x`", "user_saved_playlists": "`x` listas de reprodução guardadas", - "generic_videos_count_0": "{{count}} vídeo", - "generic_videos_count_1": "{{count}} vídeos", - "generic_videos_count_2": "{{count}} vídeos", - "generic_playlists_count_0": "{{count}} lista de reprodução", - "generic_playlists_count_1": "{{count}} listas de reprodução", - "generic_playlists_count_2": "{{count}} listas de reprodução", - "subscriptions_unseen_notifs_count_0": "{{count}} notificação não vista", - "subscriptions_unseen_notifs_count_1": "{{count}} notificações não vistas", - "subscriptions_unseen_notifs_count_2": "{{count}} notificações não vistas", - "comments_view_x_replies_0": "Ver {{count}} resposta", - "comments_view_x_replies_1": "Ver {{count}} respostas", - "comments_view_x_replies_2": "Ver {{count}} respostas", - "generic_subscribers_count_0": "{{count}} inscrito", - "generic_subscribers_count_1": "{{count}} inscritos", - "generic_subscribers_count_2": "{{count}} inscritos", - "generic_subscriptions_count_0": "{{count}} inscrição", - "generic_subscriptions_count_1": "{{count}} inscrições", - "generic_subscriptions_count_2": "{{count}} inscrições", - "comments_points_count_0": "{{count}} ponto", - "comments_points_count_1": "{{count}} pontos", - "comments_points_count_2": "{{count}} pontos", + "generic_videos_count": "{{count}} vídeo", + "generic_videos_count_plural": "{{count}} vídeos", + "generic_playlists_count": "{{count}} lista de reprodução", + "generic_playlists_count_plural": "{{count}} listas de reprodução", + "subscriptions_unseen_notifs_count": "{{count}} notificação não vista", + "subscriptions_unseen_notifs_count_plural": "{{count}} notificações não vistas", + "comments_view_x_replies": "Ver {{count}} resposta", + "comments_view_x_replies_plural": "Ver {{count}} respostas", + "generic_subscribers_count": "{{count}} inscrito", + "generic_subscribers_count_plural": "{{count}} inscritos", + "generic_subscriptions_count": "{{count}} inscrição", + "generic_subscriptions_count_plural": "{{count}} inscrições", + "comments_points_count": "{{count}} ponto", + "comments_points_count_plural": "{{count}} pontos", "crash_page_you_found_a_bug": "Parece que encontrou um erro no Invidious!", "crash_page_before_reporting": "Antes de reportar um erro, verifique se:", "crash_page_refresh": "tentou recarregar a página", From 2a092577c69d41c06f8f094348c2dd88fc6b1a17 Mon Sep 17 00:00:00 2001 From: Ming Kin Choi Date: Sun, 27 Aug 2023 12:50:36 +0800 Subject: [PATCH 364/598] Fix iOS screen timeout on video playback loop mode --- assets/js/player.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/assets/js/player.js b/assets/js/player.js index bb53ac24..0c37033d 100644 --- a/assets/js/player.js +++ b/assets/js/player.js @@ -701,6 +701,21 @@ if (navigator.vendor === 'Apple Computer, Inc.' && video_data.params.listen) { }); } +// Safari screen timeout on looped video playback fix +if (navigator.vendor === 'Apple Computer, Inc.' && !video_data.params.listen && video_data.params.video_loop) { + player.loop(false); + player.on('loadedmetadata', function () { + player.on('timeupdate', function () { + if (player.remainingTime() < 2) { + player.loop(true); + setTimeout(() => { + player.loop(false); + }, 2000 / player.playbackRate()); + } + }); + }); +} + // Watch on Invidious link if (location.pathname.startsWith('/embed/')) { const Button = videojs.getComponent('Button'); From 27d8fa112dad0b531d4e3f24045975a1869ab2ff Mon Sep 17 00:00:00 2001 From: Ming Kin Choi Date: Sun, 27 Aug 2023 14:11:45 +0800 Subject: [PATCH 365/598] Fix iOS screen timeout on video playback loop mode (more elegantly) --- assets/js/player.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/assets/js/player.js b/assets/js/player.js index 0c37033d..5d88d069 100644 --- a/assets/js/player.js +++ b/assets/js/player.js @@ -704,14 +704,10 @@ if (navigator.vendor === 'Apple Computer, Inc.' && video_data.params.listen) { // Safari screen timeout on looped video playback fix if (navigator.vendor === 'Apple Computer, Inc.' && !video_data.params.listen && video_data.params.video_loop) { player.loop(false); - player.on('loadedmetadata', function () { - player.on('timeupdate', function () { - if (player.remainingTime() < 2) { - player.loop(true); - setTimeout(() => { - player.loop(false); - }, 2000 / player.playbackRate()); - } + player.ready(function () { + player.on('ended', function () { + player.currentTime(0); + player.play(); }); }); } From eabcea6f4a16a47555d945460ac824588ff546e5 Mon Sep 17 00:00:00 2001 From: syeopite <70992037+syeopite@users.noreply.github.com> Date: Tue, 29 Aug 2023 06:18:35 +0000 Subject: [PATCH 366/598] Remove trailing whitespace in config documentation Co-authored-by: Samantaz Fox --- config/config.example.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.example.yml b/config/config.example.yml index 51beab89..c6051bce 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -187,7 +187,7 @@ https_only: false ## ## Useful for larger instances as InnerTube is **not ratelimited**. See https://github.com/iv-org/invidious/issues/2567 ## -## Subtitle experience may differ slightly on Invidious. +## Subtitle experience may differ slightly on Invidious. ## ## Accepted values: true, false ## Default: false From d7696574f4a281d7450176097c87bca08705734a Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 1 Aug 2023 08:55:23 -0700 Subject: [PATCH 367/598] Playlist: Use subtitle when author is missing --- src/invidious/playlists.cr | 5 +++++ src/invidious/views/playlist.ecr | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/invidious/playlists.cr b/src/invidious/playlists.cr index 013be268..955e0855 100644 --- a/src/invidious/playlists.cr +++ b/src/invidious/playlists.cr @@ -89,6 +89,7 @@ struct Playlist property views : Int64 property updated : Time property thumbnail : String? + property subtitle : String? def to_json(offset, json : JSON::Builder, video_id : String? = nil) json.object do @@ -100,6 +101,7 @@ struct Playlist json.field "author", self.author json.field "authorId", self.ucid json.field "authorUrl", "/channel/#{self.ucid}" + json.field "subtitle", self.subtitle json.field "authorThumbnails" do json.array do @@ -356,6 +358,8 @@ def fetch_playlist(plid : String) updated = Time.utc video_count = 0 + subtitle = extract_text(initial_data.dig?("header", "playlistHeaderRenderer", "subtitle")) + playlist_info["stats"]?.try &.as_a.each do |stat| text = stat["runs"]?.try &.as_a.map(&.["text"].as_s).join("") || stat["simpleText"]?.try &.as_s next if !text @@ -397,6 +401,7 @@ def fetch_playlist(plid : String) views: views, updated: updated, thumbnail: thumbnail, + subtitle: subtitle, }) end diff --git a/src/invidious/views/playlist.ecr b/src/invidious/views/playlist.ecr index ee9ba87b..3bc7596e 100644 --- a/src/invidious/views/playlist.ecr +++ b/src/invidious/views/playlist.ecr @@ -70,7 +70,11 @@ <% else %> - <%= author %> | + <% if !author.empty? %> + <%= author %> | + <% elsif !playlist.subtitle.nil? %> + <%= playlist.subtitle.try &.split(" • ")[0] %> | + <% end %> <%= translate_count(locale, "generic_videos_count", playlist.video_count) %> | <%= translate(locale, "Updated `x` ago", recode_date(playlist.updated, locale)) %> From afb04c3bdaa29f19db44f6560ce7954bc656d791 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Mon, 7 Aug 2023 11:58:20 -0700 Subject: [PATCH 368/598] HTMLl.Escape the playlist subtitle --- src/invidious/views/playlist.ecr | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/invidious/views/playlist.ecr b/src/invidious/views/playlist.ecr index 3bc7596e..24ba437d 100644 --- a/src/invidious/views/playlist.ecr +++ b/src/invidious/views/playlist.ecr @@ -73,7 +73,8 @@ <% if !author.empty? %> <%= author %> | <% elsif !playlist.subtitle.nil? %> - <%= playlist.subtitle.try &.split(" • ")[0] %> | + <% subtitle = playlist.subtitle || "" %> + <%= HTML.escape(subtitle[0..subtitle.rindex(" • ") || subtitle.size]) %> | <% end %> <%= translate_count(locale, "generic_videos_count", playlist.video_count) %> | <%= translate(locale, "Updated `x` ago", recode_date(playlist.updated, locale)) %> From 49b9316b9f2e9ccc6921a2f293abacb37f9805f6 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Wed, 13 Sep 2023 23:40:20 +0200 Subject: [PATCH 369/598] Routing: Handle current and future routes more nicely --- src/invidious/routes/channels.cr | 19 +++++++++++++---- src/invidious/routing.cr | 36 +++++++++++++++++++++----------- 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/src/invidious/routes/channels.cr b/src/invidious/routes/channels.cr index 9892ae2a..5500672f 100644 --- a/src/invidious/routes/channels.cr +++ b/src/invidious/routes/channels.cr @@ -217,6 +217,11 @@ module Invidious::Routes::Channels env.redirect "/channel/#{ucid}" end + private KNOWN_TABS = { + "home", "videos", "shorts", "streams", "podcasts", + "releases", "playlists", "community", "channels", "about", + } + # Redirects brand url channels to a normal /channel/:ucid route def self.brand_redirect(env) locale = env.get("preferences").as(Preferences).locale @@ -227,7 +232,10 @@ module Invidious::Routes::Channels yt_url_params = URI::Params.encode(env.params.query.to_h.select(["a", "u", "user"])) # Retrieves URL params that only Invidious uses - invidious_url_params = URI::Params.encode(env.params.query.to_h.select!(["a", "u", "user"])) + invidious_url_params = env.params.query.dup + invidious_url_params.delete_all("a") + invidious_url_params.delete_all("u") + invidious_url_params.delete_all("user") begin resolved_url = YoutubeAPI.resolve_url("https://youtube.com#{env.request.path}#{yt_url_params.size > 0 ? "?#{yt_url_params}" : ""}") @@ -236,14 +244,17 @@ module Invidious::Routes::Channels return error_template(404, translate(locale, "This channel does not exist.")) end - selected_tab = env.request.path.split("/")[-1] - if {"home", "videos", "shorts", "streams", "playlists", "community", "channels", "about"}.includes? selected_tab + selected_tab = env.params.url["tab"]? + + if KNOWN_TABS.includes? selected_tab url = "/channel/#{ucid}/#{selected_tab}" else url = "/channel/#{ucid}" end - env.redirect url + url += "?#{invidious_url_params}" if !invidious_url_params.empty? + + return env.redirect url end # Handles redirects for the /profile endpoint diff --git a/src/invidious/routing.cr b/src/invidious/routing.cr index 9c43171c..5ec7fae3 100644 --- a/src/invidious/routing.cr +++ b/src/invidious/routing.cr @@ -124,22 +124,34 @@ module Invidious::Routing get "/channel/:ucid/community", Routes::Channels, :community get "/channel/:ucid/channels", Routes::Channels, :channels get "/channel/:ucid/about", Routes::Channels, :about + get "/channel/:ucid/live", Routes::Channels, :live get "/user/:user/live", Routes::Channels, :live get "/c/:user/live", Routes::Channels, :live - {"", "/videos", "/shorts", "/streams", "/playlists", "/community", "/about"}.each do |path| - # /c/LinusTechTips - get "/c/:user#{path}", Routes::Channels, :brand_redirect - # /user/linustechtips | Not always the same as /c/ - get "/user/:user#{path}", Routes::Channels, :brand_redirect - # /@LinusTechTips | Handle - get "/@:user#{path}", Routes::Channels, :brand_redirect - # /attribution_link?a=anything&u=/channel/UCZYTClx2T1of7BRZ86-8fow - get "/attribution_link#{path}", Routes::Channels, :brand_redirect - # /profile?user=linustechtips - get "/profile/#{path}", Routes::Channels, :profile - end + # Channel catch-all, to redirect future routes to the channel's home + # NOTE: defined last in order to be processed after the other routes + get "/channel/:ucid/*", Routes::Channels, :home + + # /c/LinusTechTips + get "/c/:user", Routes::Channels, :brand_redirect + get "/c/:user/:tab", Routes::Channels, :brand_redirect + + # /user/linustechtips (Not always the same as /c/) + get "/user/:user", Routes::Channels, :brand_redirect + get "/user/:user/:tab", Routes::Channels, :brand_redirect + + # /@LinusTechTips (Handle) + get "/@:user", Routes::Channels, :brand_redirect + get "/@:user/:tab", Routes::Channels, :brand_redirect + + # /attribution_link?a=anything&u=/channel/UCZYTClx2T1of7BRZ86-8fow + get "/attribution_link", Routes::Channels, :brand_redirect + get "/attribution_link/:tab", Routes::Channels, :brand_redirect + + # /profile?user=linustechtips + get "/profile", Routes::Channels, :profile + get "/profile/*", Routes::Channels, :profile end def register_watch_routes From 2425c47882feaa56a69f6ba842cf1cb9d5b450e0 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Wed, 13 Sep 2023 23:41:31 +0200 Subject: [PATCH 370/598] Routing: Add support for the '/live/' route --- src/invidious/routing.cr | 1 + 1 file changed, 1 insertion(+) diff --git a/src/invidious/routing.cr b/src/invidious/routing.cr index 5ec7fae3..f6b3aaa6 100644 --- a/src/invidious/routing.cr +++ b/src/invidious/routing.cr @@ -158,6 +158,7 @@ module Invidious::Routing get "/watch", Routes::Watch, :handle post "/watch_ajax", Routes::Watch, :mark_watched get "/watch/:id", Routes::Watch, :redirect + get "/live/:id", Routes::Watch, :redirect get "/shorts/:id", Routes::Watch, :redirect get "/clip/:clip", Routes::Watch, :clip get "/w/:id", Routes::Watch, :redirect From beec62cf0e45fe5620f9381050080c685f32070e Mon Sep 17 00:00:00 2001 From: RadoslavL Date: Thu, 14 Sep 2023 20:37:35 +0300 Subject: [PATCH 371/598] Increased link contrast in dark mode --- assets/css/default.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/css/default.css b/assets/css/default.css index c31b24e5..c94ed9d8 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -581,7 +581,7 @@ span > select { } .dark-theme a { - color: #a0a0a0; + color: #adadad; text-decoration: none; } From 792a999386f9147233d26300856a5802da5fc8c1 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Thu, 14 Sep 2023 20:39:46 +0200 Subject: [PATCH 372/598] Frontend: Add timestamp on youtube+embed links --- assets/js/player.js | 15 +++++++++++++++ src/invidious/views/watch.ecr | 14 ++++++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/assets/js/player.js b/assets/js/player.js index bb53ac24..cd0e7a72 100644 --- a/assets/js/player.js +++ b/assets/js/player.js @@ -112,6 +112,21 @@ function addCurrentTimeToURL(url) { return urlUsed; } +/** + * Timer that updates the timestamp on "watch on youtube" and "embed" links + */ +player.ready(function () { + let elem_watch = document.getElementById('link-yt-watch'); + let elem_embed = document.getElementById('link-yt-embed'); + + let base_url_watch = elem_watch.getAttribute('data-base-url'); + let base_url_embed = elem_embed.getAttribute('data-base-url'); + + setTimeout(() => { elem_watch.setAttribute('href') = addCurrentTimeToURL(base_url_watch); }, 5000); + setTimeout(() => { elem_embed.setAttribute('href') = addCurrentTimeToURL(base_url_embed); }, 5000); +}); + + var shareOptions = { socials: ['fbFeed', 'tw', 'reddit', 'email'], diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 498d57a1..ac3fee65 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -112,8 +112,18 @@ we're going to need to do it here in order to allow for translations.
    - <%= translate(locale, "videoinfo_watch_on_youTube") %> - (<%= translate(locale, "videoinfo_youTube_embed_link") %>) + <%- + link_yt_watch = URI.new(scheme: "https", host: "www.youtube.com", path: "/watch", query: "v=#{video.id}") + link_yt_embed = URI.new(scheme: "https", host: "www.youtube.com", path: "/embed/#{video.id}") + + if !plid.nil? && !continuation.nil? + link_yt_param = URI::Params{"plid" => [plid], "index" => [continuation.to_s]} + link_yt_watch = IV::HttpServer::Utils.add_params_to_url(link_yt_watch, link_yt_param) + link_yt_embed = IV::HttpServer::Utils.add_params_to_url(link_yt_embed, link_yt_param) + end + -%> + <%= translate(locale, "videoinfo_watch_on_youTube") %> + (<%= translate(locale, "videoinfo_youTube_embed_link") %>)

    <% if env.get("preferences").as(Preferences).automatic_instance_redirect%> From 2456b629365450970363e5cf0e9a65c1a24160ab Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Thu, 14 Sep 2023 20:50:17 +0200 Subject: [PATCH 373/598] Frontend: Add timestamp on invidious embed links --- assets/js/player.js | 15 +++++++++------ src/invidious/routes/watch.cr | 8 -------- src/invidious/views/watch.ecr | 12 +++++++++++- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/assets/js/player.js b/assets/js/player.js index cd0e7a72..d07d6cf4 100644 --- a/assets/js/player.js +++ b/assets/js/player.js @@ -116,14 +116,17 @@ function addCurrentTimeToURL(url) { * Timer that updates the timestamp on "watch on youtube" and "embed" links */ player.ready(function () { - let elem_watch = document.getElementById('link-yt-watch'); - let elem_embed = document.getElementById('link-yt-embed'); + let elem_yt_watch = document.getElementById('link-yt-watch'); + let elem_yt_embed = document.getElementById('link-yt-embed'); + let elem_iv_embed = document.getElementById('link-iv-embed'); - let base_url_watch = elem_watch.getAttribute('data-base-url'); - let base_url_embed = elem_embed.getAttribute('data-base-url'); + let base_url_yt_watch = elem_yt_watch.getAttribute('data-base-url'); + let base_url_yt_embed = elem_yt_embed.getAttribute('data-base-url'); + let base_url_iv_embed = elem_iv_embed.getAttribute('data-base-url'); - setTimeout(() => { elem_watch.setAttribute('href') = addCurrentTimeToURL(base_url_watch); }, 5000); - setTimeout(() => { elem_embed.setAttribute('href') = addCurrentTimeToURL(base_url_embed); }, 5000); + setTimeout(() => { elem_yt_watch.setAttribute('href') = addCurrentTimeToURL(base_url_yt_watch); }, 5000); + setTimeout(() => { elem_yt_embed.setAttribute('href') = addCurrentTimeToURL(base_url_yt_embed); }, 5000); + setTimeout(() => { elem_iv_embed.setAttribute('href') = addCurrentTimeToURL(base_url_iv_embed); }, 5000); }); diff --git a/src/invidious/routes/watch.cr b/src/invidious/routes/watch.cr index e5cf3716..3d935f0a 100644 --- a/src/invidious/routes/watch.cr +++ b/src/invidious/routes/watch.cr @@ -30,14 +30,6 @@ module Invidious::Routes::Watch return env.redirect "/" end - embed_link = "/embed/#{id}" - if env.params.query.size > 1 - embed_params = HTTP::Params.parse(env.params.query.to_s) - embed_params.delete_all("v") - embed_link += "?" - embed_link += embed_params.to_s - end - plid = env.params.query["list"]?.try &.gsub(/[^a-zA-Z0-9_-]/, "") continuation = process_continuation(env.params.query, plid, id) diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index ac3fee65..a768328a 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -125,6 +125,7 @@ we're going to need to do it here in order to allow for translations. <%= translate(locale, "videoinfo_watch_on_youTube") %> (<%= translate(locale, "videoinfo_youTube_embed_link") %>) +

    <% if env.get("preferences").as(Preferences).automatic_instance_redirect%> "><%= translate(locale, "Switch Invidious Instance") %> @@ -132,9 +133,18 @@ we're going to need to do it here in order to allow for translations. <%= translate(locale, "Switch Invidious Instance") %> <% end %>

    + +

    <% if params.annotations %> From 58f4a012b7fde782a83d6745f18c5d080f7ade6a Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Thu, 14 Sep 2023 22:10:02 +0200 Subject: [PATCH 374/598] Frontend: Add timestamp on switch invidious instance links --- assets/js/player.js | 26 ++++++++++++++++++++------ src/invidious/views/watch.ecr | 7 ++----- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/assets/js/player.js b/assets/js/player.js index d07d6cf4..bffc7ad3 100644 --- a/assets/js/player.js +++ b/assets/js/player.js @@ -113,20 +113,34 @@ function addCurrentTimeToURL(url) { } /** - * Timer that updates the timestamp on "watch on youtube" and "embed" links + * Timer that updates the timestamp on all external links */ player.ready(function () { + // YouTube links + let elem_yt_watch = document.getElementById('link-yt-watch'); let elem_yt_embed = document.getElementById('link-yt-embed'); - let elem_iv_embed = document.getElementById('link-iv-embed'); let base_url_yt_watch = elem_yt_watch.getAttribute('data-base-url'); let base_url_yt_embed = elem_yt_embed.getAttribute('data-base-url'); - let base_url_iv_embed = elem_iv_embed.getAttribute('data-base-url'); - setTimeout(() => { elem_yt_watch.setAttribute('href') = addCurrentTimeToURL(base_url_yt_watch); }, 5000); - setTimeout(() => { elem_yt_embed.setAttribute('href') = addCurrentTimeToURL(base_url_yt_embed); }, 5000); - setTimeout(() => { elem_iv_embed.setAttribute('href') = addCurrentTimeToURL(base_url_iv_embed); }, 5000); + setTimeout(() => { + elem_yt_watch.setAttribute('href') = addCurrentTimeToURL(base_url_yt_watch); + elem_yt_embed.setAttribute('href') = addCurrentTimeToURL(base_url_yt_embed); + }, 5000); + + // Invidious links + + let elem_iv_embed = document.getElementById('link-iv-embed'); + let elem_iv_other = document.getElementById('link-iv-other'); + + let base_url_iv_embed = elem_iv_embed.getAttribute('data-base-url'); + let base_url_iv_other = elem_iv_other.getAttribute('data-base-url'); + + setTimeout(() => { + elem_iv_embed.setAttribute('href') = addCurrentTimeToURL(base_url_iv_embed); + elem_iv_other.setAttribute('href') = addCurrentTimeToURL(base_url_iv_other); + }, 5000); }); diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index a768328a..bf297a43 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -127,11 +127,8 @@ we're going to need to do it here in order to allow for translations.

    - <% if env.get("preferences").as(Preferences).automatic_instance_redirect%> - "><%= translate(locale, "Switch Invidious Instance") %> - <% else %> - <%= translate(locale, "Switch Invidious Instance") %> - <% end %> + <%- link_iv_other = IV::Frontend::Misc.redirect_url(env) -%> + <%= translate(locale, "Switch Invidious Instance") %>

    '; +var spinnerHTMLwithHR = spinnerHTML + '
    '; + +String.prototype.supplant = function (o) { + return this.replace(/{([^{}]*)}/g, function (a, b) { + var r = o[b]; + return typeof r === 'string' || typeof r === 'number' ? r : a; + }); +}; + +function toggle_comments(event) { + var target = event.target; + var body = target.parentNode.parentNode.parentNode.children[1]; + if (body.style.display === 'none') { + target.textContent = '[ − ]'; + body.style.display = ''; + } else { + target.textContent = '[ + ]'; + body.style.display = 'none'; + } +} + +function hide_youtube_replies(event) { + var target = event.target; + + var sub_text = target.getAttribute('data-inner-text'); + var inner_text = target.getAttribute('data-sub-text'); + + var body = target.parentNode.parentNode.children[1]; + body.style.display = 'none'; + + target.textContent = sub_text; + target.onclick = show_youtube_replies; + target.setAttribute('data-inner-text', inner_text); + target.setAttribute('data-sub-text', sub_text); +} + +function show_youtube_replies(event) { + var target = event.target; + + var sub_text = target.getAttribute('data-inner-text'); + var inner_text = target.getAttribute('data-sub-text'); + + var body = target.parentNode.parentNode.children[1]; + body.style.display = ''; + + target.textContent = sub_text; + target.onclick = hide_youtube_replies; + target.setAttribute('data-inner-text', inner_text); + target.setAttribute('data-sub-text', sub_text); +} + +function get_youtube_comments() { + var comments = document.getElementById('comments'); + + var fallback = comments.innerHTML; + comments.innerHTML = spinnerHTML; + + var baseUrl = video_data.base_url || '/api/v1/comments/'+ video_data.id + var url = baseUrl + + '?format=html' + + '&hl=' + video_data.preferences.locale + + '&thin_mode=' + video_data.preferences.thin_mode; + + if (video_data.ucid) { + url += '&ucid=' + video_data.ucid + } + + var onNon200 = function (xhr) { comments.innerHTML = fallback; }; + if (video_data.params.comments[1] === 'youtube') + onNon200 = function (xhr) {}; + + helpers.xhr('GET', url, {retries: 5, entity_name: 'comments'}, { + on200: function (response) { + var commentInnerHtml = ' \ +
    \ +

    \ + [ − ] \ + {commentsText} \ +

    \ + \ + ' + if (video_data.support_reddit) { + commentInnerHtml += ' \ + {redditComments} \ + \ + ' + } + commentInnerHtml += ' \ +
    \ +
    {contentHtml}
    \ +
    ' + commentInnerHtml = commentInnerHtml.supplant({ + contentHtml: response.contentHtml, + redditComments: video_data.reddit_comments_text, + commentsText: video_data.comments_text.supplant({ + // toLocaleString correctly splits number with local thousands separator. e.g.: + // '1,234,567.89' for user with English locale + // '1 234 567,89' for user with Russian locale + // '1.234.567,89' for user with Portuguese locale + commentCount: response.commentCount.toLocaleString() + }) + }); + comments.innerHTML = commentInnerHtml; + comments.children[0].children[0].children[0].onclick = toggle_comments; + if (video_data.support_reddit) { + comments.children[0].children[1].children[0].onclick = swap_comments; + } + }, + onNon200: onNon200, // declared above + onError: function (xhr) { + comments.innerHTML = spinnerHTML; + }, + onTimeout: function (xhr) { + comments.innerHTML = spinnerHTML; + } + }); +} + +function get_youtube_replies(target, load_more, load_replies) { + var continuation = target.getAttribute('data-continuation'); + + var body = target.parentNode.parentNode; + var fallback = body.innerHTML; + body.innerHTML = spinnerHTML; + var baseUrl = video_data.base_url || '/api/v1/comments/'+ video_data.id + var url = baseUrl + + '?format=html' + + '&hl=' + video_data.preferences.locale + + '&thin_mode=' + video_data.preferences.thin_mode + + '&continuation=' + continuation; + + if (video_data.ucid) { + url += '&ucid=' + video_data.ucid + } + if (load_replies) url += '&action=action_get_comment_replies'; + + helpers.xhr('GET', url, {}, { + on200: function (response) { + if (load_more) { + body = body.parentNode.parentNode; + body.removeChild(body.lastElementChild); + body.insertAdjacentHTML('beforeend', response.contentHtml); + } else { + body.removeChild(body.lastElementChild); + + var p = document.createElement('p'); + var a = document.createElement('a'); + p.appendChild(a); + + a.href = 'javascript:void(0)'; + a.onclick = hide_youtube_replies; + a.setAttribute('data-sub-text', video_data.hide_replies_text); + a.setAttribute('data-inner-text', video_data.show_replies_text); + a.textContent = video_data.hide_replies_text; + + var div = document.createElement('div'); + div.innerHTML = response.contentHtml; + + body.appendChild(p); + body.appendChild(div); + } + }, + onNon200: function (xhr) { + body.innerHTML = fallback; + }, + onTimeout: function (xhr) { + console.warn('Pulling comments failed'); + body.innerHTML = fallback; + } + }); +} \ No newline at end of file diff --git a/assets/js/post.js b/assets/js/post.js new file mode 100644 index 00000000..fcbc9155 --- /dev/null +++ b/assets/js/post.js @@ -0,0 +1,3 @@ +addEventListener('load', function (e) { + get_youtube_comments(); +}); diff --git a/assets/js/watch.js b/assets/js/watch.js index 36506abd..26ad138f 100644 --- a/assets/js/watch.js +++ b/assets/js/watch.js @@ -1,14 +1,4 @@ 'use strict'; -var video_data = JSON.parse(document.getElementById('video_data').textContent); -var spinnerHTML = '

    '; -var spinnerHTMLwithHR = spinnerHTML + '
    '; - -String.prototype.supplant = function (o) { - return this.replace(/{([^{}]*)}/g, function (a, b) { - var r = o[b]; - return typeof r === 'string' || typeof r === 'number' ? r : a; - }); -}; function toggle_parent(target) { var body = target.parentNode.parentNode.children[1]; @@ -21,18 +11,6 @@ function toggle_parent(target) { } } -function toggle_comments(event) { - var target = event.target; - var body = target.parentNode.parentNode.parentNode.children[1]; - if (body.style.display === 'none') { - target.textContent = '[ − ]'; - body.style.display = ''; - } else { - target.textContent = '[ + ]'; - body.style.display = 'none'; - } -} - function swap_comments(event) { var source = event.target.getAttribute('data-comments'); @@ -43,36 +21,6 @@ function swap_comments(event) { } } -function hide_youtube_replies(event) { - var target = event.target; - - var sub_text = target.getAttribute('data-inner-text'); - var inner_text = target.getAttribute('data-sub-text'); - - var body = target.parentNode.parentNode.children[1]; - body.style.display = 'none'; - - target.textContent = sub_text; - target.onclick = show_youtube_replies; - target.setAttribute('data-inner-text', inner_text); - target.setAttribute('data-sub-text', sub_text); -} - -function show_youtube_replies(event) { - var target = event.target; - - var sub_text = target.getAttribute('data-inner-text'); - var inner_text = target.getAttribute('data-sub-text'); - - var body = target.parentNode.parentNode.children[1]; - body.style.display = ''; - - target.textContent = sub_text; - target.onclick = hide_youtube_replies; - target.setAttribute('data-inner-text', inner_text); - target.setAttribute('data-sub-text', sub_text); -} - var continue_button = document.getElementById('continue'); if (continue_button) { continue_button.onclick = continue_autoplay; @@ -208,111 +156,6 @@ function get_reddit_comments() { }); } -function get_youtube_comments() { - var comments = document.getElementById('comments'); - - var fallback = comments.innerHTML; - comments.innerHTML = spinnerHTML; - - var url = '/api/v1/comments/' + video_data.id + - '?format=html' + - '&hl=' + video_data.preferences.locale + - '&thin_mode=' + video_data.preferences.thin_mode; - - var onNon200 = function (xhr) { comments.innerHTML = fallback; }; - if (video_data.params.comments[1] === 'youtube') - onNon200 = function (xhr) {}; - - helpers.xhr('GET', url, {retries: 5, entity_name: 'comments'}, { - on200: function (response) { - comments.innerHTML = ' \ -
    \ -

    \ - [ − ] \ - {commentsText} \ -

    \ - \ - \ - {redditComments} \ - \ - \ -
    \ -
    {contentHtml}
    \ -
    '.supplant({ - contentHtml: response.contentHtml, - redditComments: video_data.reddit_comments_text, - commentsText: video_data.comments_text.supplant({ - // toLocaleString correctly splits number with local thousands separator. e.g.: - // '1,234,567.89' for user with English locale - // '1 234 567,89' for user with Russian locale - // '1.234.567,89' for user with Portuguese locale - commentCount: response.commentCount.toLocaleString() - }) - }); - - comments.children[0].children[0].children[0].onclick = toggle_comments; - comments.children[0].children[1].children[0].onclick = swap_comments; - }, - onNon200: onNon200, // declared above - onError: function (xhr) { - comments.innerHTML = spinnerHTML; - }, - onTimeout: function (xhr) { - comments.innerHTML = spinnerHTML; - } - }); -} - -function get_youtube_replies(target, load_more, load_replies) { - var continuation = target.getAttribute('data-continuation'); - - var body = target.parentNode.parentNode; - var fallback = body.innerHTML; - body.innerHTML = spinnerHTML; - - var url = '/api/v1/comments/' + video_data.id + - '?format=html' + - '&hl=' + video_data.preferences.locale + - '&thin_mode=' + video_data.preferences.thin_mode + - '&continuation=' + continuation; - if (load_replies) url += '&action=action_get_comment_replies'; - - helpers.xhr('GET', url, {}, { - on200: function (response) { - if (load_more) { - body = body.parentNode.parentNode; - body.removeChild(body.lastElementChild); - body.insertAdjacentHTML('beforeend', response.contentHtml); - } else { - body.removeChild(body.lastElementChild); - - var p = document.createElement('p'); - var a = document.createElement('a'); - p.appendChild(a); - - a.href = 'javascript:void(0)'; - a.onclick = hide_youtube_replies; - a.setAttribute('data-sub-text', video_data.hide_replies_text); - a.setAttribute('data-inner-text', video_data.show_replies_text); - a.textContent = video_data.hide_replies_text; - - var div = document.createElement('div'); - div.innerHTML = response.contentHtml; - - body.appendChild(p); - body.appendChild(div); - } - }, - onNon200: function (xhr) { - body.innerHTML = fallback; - }, - onTimeout: function (xhr) { - console.warn('Pulling comments failed'); - body.innerHTML = fallback; - } - }); -} - if (video_data.play_next) { player.on('ended', function () { var url = new URL('https://example.com/watch?v=' + video_data.next_video); diff --git a/src/invidious/channels/community.cr b/src/invidious/channels/community.cr index 791f1641..85ddff35 100644 --- a/src/invidious/channels/community.cr +++ b/src/invidious/channels/community.cr @@ -24,7 +24,35 @@ def fetch_channel_community(ucid, cursor, locale, format, thin_mode) return extract_channel_community(items, ucid: ucid, locale: locale, format: format, thin_mode: thin_mode) end -def extract_channel_community(items, *, ucid, locale, format, thin_mode) +def fetch_channel_community_post(ucid, postId, locale, format, thin_mode, params : String | Nil = nil) + if params.nil? + object = { + "2:string" => "community", + "25:embedded" => { + "22:string" => postId.to_s, + }, + "45:embedded" => { + "2:varint" => 1_i64, + "3:varint" => 1_i64, + }, + } + params = 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) } + end + + initial_data = YoutubeAPI.browse(ucid, params: params) + + items = [] of JSON::Any + extract_items(initial_data) do |item| + items << item + end + + return extract_channel_community(items, ucid: ucid, locale: locale, format: format, thin_mode: thin_mode, is_single_post: true) +end + +def extract_channel_community(items, *, ucid, locale, format, thin_mode, is_single_post : Bool = false) if message = items[0]["messageRenderer"]? error_message = (message["text"]["simpleText"]? || message["text"]["runs"]?.try &.[0]?.try &.["text"]?) @@ -39,6 +67,9 @@ def extract_channel_community(items, *, ucid, locale, format, thin_mode) response = JSON.build do |json| json.object do json.field "authorId", ucid + if is_single_post + json.field "singlePost", true + end json.field "comments" do json.array do items.each do |post| @@ -240,8 +271,10 @@ def extract_channel_community(items, *, ucid, locale, format, thin_mode) end end end - if cont = items.dig?(-1, "continuationItemRenderer", "continuationEndpoint", "continuationCommand", "token") - json.field "continuation", extract_channel_community_cursor(cont.as_s) + if !is_single_post + if cont = items.dig?(-1, "continuationItemRenderer", "continuationEndpoint", "continuationCommand", "token") + json.field "continuation", extract_channel_community_cursor(cont.as_s) + end end end end diff --git a/src/invidious/comments/youtube.cr b/src/invidious/comments/youtube.cr index 1ba1b534..da7f0543 100644 --- a/src/invidious/comments/youtube.cr +++ b/src/invidious/comments/youtube.cr @@ -13,6 +13,51 @@ module Invidious::Comments client_config = YoutubeAPI::ClientConfig.new(region: region) response = YoutubeAPI.next(continuation: ctoken, client_config: client_config) + return parse_youtube(id, response, format, locale, thin_mode, sort_by) + end + + def fetch_community_post_comments(ucid, postId) + object = { + "2:string" => "community", + "25:embedded" => { + "22:string" => postId, + }, + "45:embedded" => { + "2:varint" => 1_i64, + "3:varint" => 1_i64, + }, + "53:embedded" => { + "4:embedded" => { + "6:varint" => 0_i64, + "27:varint" => 1_i64, + "29:string" => postId, + "30:string" => ucid, + }, + "8:string" => "comments-section", + }, + } + + objectParsed = object.try { |i| Protodec::Any.cast_json(i) } + .try { |i| Protodec::Any.from_json(i) } + .try { |i| Base64.urlsafe_encode(i) } + + object2 = { + "80226972:embedded" => { + "2:string" => ucid, + "3:string" => objectParsed, + }, + } + + continuation = object2.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) } + + initial_data = YoutubeAPI.browse(continuation: continuation) + return initial_data + end + + def parse_youtube(id, response, format, locale, thin_mode, sort_by = "top", isPost = false) contents = nil if on_response_received_endpoints = response["onResponseReceivedEndpoints"]? @@ -68,7 +113,11 @@ module Invidious::Comments json.field "commentCount", comment_count end - json.field "videoId", id + if isPost + json.field "postId", id + else + json.field "videoId", id + end json.field "comments" do json.array do diff --git a/src/invidious/frontend/comments_youtube.cr b/src/invidious/frontend/comments_youtube.cr index 41f43f04..ecc0bc1b 100644 --- a/src/invidious/frontend/comments_youtube.cr +++ b/src/invidious/frontend/comments_youtube.cr @@ -23,6 +23,24 @@ module Invidious::Frontend::Comments
    END_HTML + elsif comments["authorId"]? && !comments["singlePost"]? + # for posts we should display a link to the post + replies_count_text = translate_count(locale, + "comments_view_x_replies", + child["replyCount"].as_i64 || 0, + NumberFormatting::Separator + ) + + replies_html = <<-END_HTML +
    +
    + +
    + END_HTML end if !thin_mode diff --git a/src/invidious/routes/api/v1/channels.cr b/src/invidious/routes/api/v1/channels.cr index adf05d30..0d2d2eb1 100644 --- a/src/invidious/routes/api/v1/channels.cr +++ b/src/invidious/routes/api/v1/channels.cr @@ -343,6 +343,53 @@ module Invidious::Routes::API::V1::Channels end end + def self.post(env) + locale = env.get("preferences").as(Preferences).locale + + env.response.content_type = "application/json" + + id = env.params.url["id"].to_s + ucid = env.params.query["ucid"] + + thin_mode = env.params.query["thin_mode"]? + thin_mode = thin_mode == "true" + + format = env.params.query["format"]? + format ||= "json" + + begin + fetch_channel_community_post(ucid, id, locale, format, thin_mode) + rescue ex + return error_json(500, ex) + end + end + + def self.post_comments(env) + locale = env.get("preferences").as(Preferences).locale + region = env.params.query["region"]? + + env.response.content_type = "application/json" + + id = env.params.url["id"] + + thin_mode = env.params.query["thin_mode"]? + thin_mode = thin_mode == "true" + + format = env.params.query["format"]? + format ||= "json" + + continuation = env.params.query["continuation"]? + + case continuation + when nil, "" + ucid = env.params.query["ucid"] + comments = Comments.fetch_community_post_comments(ucid, id) + else + comments = YoutubeAPI.browse(continuation: continuation) + end + return Comments.parse_youtube(id, comments, format, locale, thin_mode, isPost: true) + end + def self.channels(env) locale = env.get("preferences").as(Preferences).locale ucid = env.params.url["ucid"] diff --git a/src/invidious/routes/api/v1/misc.cr b/src/invidious/routes/api/v1/misc.cr index e499f4d6..91a62fa3 100644 --- a/src/invidious/routes/api/v1/misc.cr +++ b/src/invidious/routes/api/v1/misc.cr @@ -162,17 +162,23 @@ module Invidious::Routes::API::V1::Misc resolved_url = YoutubeAPI.resolve_url(url.as(String)) endpoint = resolved_url["endpoint"] pageType = endpoint.dig?("commandMetadata", "webCommandMetadata", "webPageType").try &.as_s || "" - if resolved_ucid = endpoint.dig?("watchEndpoint", "videoId") - elsif resolved_ucid = endpoint.dig?("browseEndpoint", "browseId") + if sub_endpoint = endpoint.dig?("watchEndpoint") + resolved_ucid = sub_endpoint.dig?("videoId") + elsif sub_endpoint = endpoint.dig?("browseEndpoint") + resolved_ucid = sub_endpoint.dig?("browseId") elsif pageType == "WEB_PAGE_TYPE_UNKNOWN" return error_json(400, "Unknown url") end + if !sub_endpoint.nil? + params = sub_endpoint.dig?("params") + end rescue ex return error_json(500, ex) end JSON.build do |json| json.object do json.field "ucid", resolved_ucid.try &.as_s || "" + json.field "params", params.try &.as_s json.field "pageType", pageType end end diff --git a/src/invidious/routes/channels.cr b/src/invidious/routes/channels.cr index 9892ae2a..1d02ee08 100644 --- a/src/invidious/routes/channels.cr +++ b/src/invidious/routes/channels.cr @@ -159,6 +159,11 @@ module Invidious::Routes::Channels end locale, user, subscriptions, continuation, ucid, channel = data + # redirect to post page + if lb = env.params.query["lb"]? + env.redirect "/post/#{lb}?ucid=#{ucid}" + end + thin_mode = env.params.query["thin_mode"]? || env.get("preferences").as(Preferences).thin_mode thin_mode = thin_mode == "true" @@ -187,6 +192,38 @@ module Invidious::Routes::Channels templated "community" end + def self.post(env) + # /post/{postId} + id = env.params.url["id"] + ucid = env.params.query["ucid"]? + + prefs = env.get("preferences").as(Preferences) + + locale = prefs.locale + region = env.params.query["region"]? || prefs.region + + thin_mode = env.params.query["thin_mode"]? || prefs.thin_mode + thin_mode = thin_mode == "true" + + client_config = YoutubeAPI::ClientConfig.new(region: region) + + if !ucid.nil? + ucid = ucid.to_s + post_response = fetch_channel_community_post(ucid, id, locale, "json", thin_mode) + else + # resolve the url to get the author's UCID + response = YoutubeAPI.resolve_url("https://www.youtube.com/post/#{id}") + return error_template(400, "Invalid post ID") if response["error"]? + + ucid = response.dig("endpoint", "browseEndpoint", "browseId").as_s + params = response.dig("endpoint", "browseEndpoint", "params").as_s + post_response = fetch_channel_community_post(ucid, id, locale, "json", thin_mode, params: params) + end + + post_response = JSON.parse(post_response) + templated "post" + end + def self.channels(env) data = self.fetch_basic_information(env) return data if !data.is_a?(Tuple) diff --git a/src/invidious/routing.cr b/src/invidious/routing.cr index 9c43171c..8cb49249 100644 --- a/src/invidious/routing.cr +++ b/src/invidious/routing.cr @@ -127,6 +127,7 @@ module Invidious::Routing get "/channel/:ucid/live", Routes::Channels, :live get "/user/:user/live", Routes::Channels, :live get "/c/:user/live", Routes::Channels, :live + get "/post/:id", Routes::Channels, :post {"", "/videos", "/shorts", "/streams", "/playlists", "/community", "/about"}.each do |path| # /c/LinusTechTips @@ -240,6 +241,10 @@ module Invidious::Routing get "/api/v1/channels/:ucid/#{{{route}}}", {{namespace}}::Channels, :{{route}} {% end %} + # Posts + get "/api/v1/post/:id", {{namespace}}::Channels, :post + get "/api/v1/post/:id/comments", {{namespace}}::Channels, :post_comments + # 301 redirects to new /api/v1/channels/community/:ucid and /:ucid/community get "/api/v1/channels/comments/:ucid", {{namespace}}::Channels, :channel_comments_redirect get "/api/v1/channels/:ucid/comments", {{namespace}}::Channels, :channel_comments_redirect @@ -249,6 +254,7 @@ module Invidious::Routing get "/api/v1/search/suggestions", {{namespace}}::Search, :search_suggestions get "/api/v1/hashtag/:hashtag", {{namespace}}::Search, :hashtag + # Authenticated # The notification APIs cannot be extracted yet! They require the *local* notifications constant defined in invidious.cr diff --git a/src/invidious/views/community.ecr b/src/invidious/views/community.ecr index 24efc34e..d2a305d3 100644 --- a/src/invidious/views/community.ecr +++ b/src/invidious/views/community.ecr @@ -26,7 +26,7 @@

    <%= error_message %>

    <% else %> -
    +
    <%= IV::Frontend::Comments.template_youtube(items.not_nil!, locale, thin_mode) %>
    <% end %> diff --git a/src/invidious/views/post.ecr b/src/invidious/views/post.ecr new file mode 100644 index 00000000..b2cd778c --- /dev/null +++ b/src/invidious/views/post.ecr @@ -0,0 +1,31 @@ +<% content_for "header" do %> +Invidious +<% end %> + +
    + <%= IV::Frontend::Comments.template_youtube(post_response.not_nil!, locale, thin_mode) %> +
    +
    +
    + + + + \ No newline at end of file diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 498d57a1..62a154a4 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -64,7 +64,8 @@ we're going to need to do it here in order to allow for translations. "premiere_timestamp" => video.premiere_timestamp.try &.to_unix, "vr" => video.is_vr, "projection_type" => video.projection_type, - "local_disabled" => CONFIG.disabled?("local") + "local_disabled" => CONFIG.disabled?("local"), + "support_reddit" => true }.to_pretty_json %> @@ -270,7 +271,7 @@ we're going to need to do it here in order to allow for translations.
    <% end %> -
    +
    <% if nojs %> <%= comment_html %> <% else %> @@ -352,4 +353,5 @@ we're going to need to do it here in order to allow for translations.
    <% end %>
    + From 734f1b7764598bd5ff24acd11ab833f831d0f4a7 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Thu, 27 Jul 2023 19:14:34 -0400 Subject: [PATCH 384/598] Simplify resolveUrl api call Co-Authored-By: Samantaz Fox --- src/invidious/channels/community.cr | 4 ++-- src/invidious/comments/youtube.cr | 6 +++--- src/invidious/routes/api/v1/misc.cr | 13 ++++++------- src/invidious/routes/channels.cr | 2 +- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/invidious/channels/community.cr b/src/invidious/channels/community.cr index 85ddff35..76dab361 100644 --- a/src/invidious/channels/community.cr +++ b/src/invidious/channels/community.cr @@ -24,12 +24,12 @@ def fetch_channel_community(ucid, cursor, locale, format, thin_mode) return extract_channel_community(items, ucid: ucid, locale: locale, format: format, thin_mode: thin_mode) end -def fetch_channel_community_post(ucid, postId, locale, format, thin_mode, params : String | Nil = nil) +def fetch_channel_community_post(ucid, post_id, locale, format, thin_mode, params : String | Nil = nil) if params.nil? object = { "2:string" => "community", "25:embedded" => { - "22:string" => postId.to_s, + "22:string" => post_id.to_s, }, "45:embedded" => { "2:varint" => 1_i64, diff --git a/src/invidious/comments/youtube.cr b/src/invidious/comments/youtube.cr index da7f0543..01c2564f 100644 --- a/src/invidious/comments/youtube.cr +++ b/src/invidious/comments/youtube.cr @@ -16,11 +16,11 @@ module Invidious::Comments return parse_youtube(id, response, format, locale, thin_mode, sort_by) end - def fetch_community_post_comments(ucid, postId) + def fetch_community_post_comments(ucid, post_id) object = { "2:string" => "community", "25:embedded" => { - "22:string" => postId, + "22:string" => post_id, }, "45:embedded" => { "2:varint" => 1_i64, @@ -30,7 +30,7 @@ module Invidious::Comments "4:embedded" => { "6:varint" => 0_i64, "27:varint" => 1_i64, - "29:string" => postId, + "29:string" => post_id, "30:string" => ucid, }, "8:string" => "comments-section", diff --git a/src/invidious/routes/api/v1/misc.cr b/src/invidious/routes/api/v1/misc.cr index 91a62fa3..6118a0d1 100644 --- a/src/invidious/routes/api/v1/misc.cr +++ b/src/invidious/routes/api/v1/misc.cr @@ -162,16 +162,15 @@ module Invidious::Routes::API::V1::Misc resolved_url = YoutubeAPI.resolve_url(url.as(String)) endpoint = resolved_url["endpoint"] pageType = endpoint.dig?("commandMetadata", "webCommandMetadata", "webPageType").try &.as_s || "" - if sub_endpoint = endpoint.dig?("watchEndpoint") - resolved_ucid = sub_endpoint.dig?("videoId") - elsif sub_endpoint = endpoint.dig?("browseEndpoint") - resolved_ucid = sub_endpoint.dig?("browseId") + if sub_endpoint = endpoint["watchEndpoint"]? + resolved_ucid = sub_endpoint["videoId"]? + elsif sub_endpoint = endpoint["browseEndpoint"]? + resolved_ucid = sub_endpoint["browseId"]? elsif pageType == "WEB_PAGE_TYPE_UNKNOWN" return error_json(400, "Unknown url") end - if !sub_endpoint.nil? - params = sub_endpoint.dig?("params") - end + + params = sub_endpoint.try &.dig?("params") rescue ex return error_json(500, ex) end diff --git a/src/invidious/routes/channels.cr b/src/invidious/routes/channels.cr index 1d02ee08..8515b910 100644 --- a/src/invidious/routes/channels.cr +++ b/src/invidious/routes/channels.cr @@ -161,7 +161,7 @@ module Invidious::Routes::Channels # redirect to post page if lb = env.params.query["lb"]? - env.redirect "/post/#{lb}?ucid=#{ucid}" + env.redirect "/post/#{URI.encode_www_form(lb)}?ucid=#{URI.encode_www_form(ucid)}" end thin_mode = env.params.query["thin_mode"]? || env.get("preferences").as(Preferences).thin_mode From f55b96a53bde8d8c6a24d4db4e9d10f14ffee585 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Mon, 7 Aug 2023 12:46:19 -0700 Subject: [PATCH 385/598] Always craft Community Post params --- src/invidious/channels/community.cr | 32 ++++++++++++++--------------- src/invidious/routes/channels.cr | 3 +-- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/src/invidious/channels/community.cr b/src/invidious/channels/community.cr index 76dab361..49ffd990 100644 --- a/src/invidious/channels/community.cr +++ b/src/invidious/channels/community.cr @@ -24,23 +24,21 @@ def fetch_channel_community(ucid, cursor, locale, format, thin_mode) return extract_channel_community(items, ucid: ucid, locale: locale, format: format, thin_mode: thin_mode) end -def fetch_channel_community_post(ucid, post_id, locale, format, thin_mode, params : String | Nil = nil) - if params.nil? - object = { - "2:string" => "community", - "25:embedded" => { - "22:string" => post_id.to_s, - }, - "45:embedded" => { - "2:varint" => 1_i64, - "3:varint" => 1_i64, - }, - } - params = 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) } - end +def fetch_channel_community_post(ucid, post_id, locale, format, thin_mode) + object = { + "2:string" => "community", + "25:embedded" => { + "22:string" => post_id.to_s, + }, + "45:embedded" => { + "2:varint" => 1_i64, + "3:varint" => 1_i64, + }, + } + params = 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) } initial_data = YoutubeAPI.browse(ucid, params: params) diff --git a/src/invidious/routes/channels.cr b/src/invidious/routes/channels.cr index 8515b910..20b02dc1 100644 --- a/src/invidious/routes/channels.cr +++ b/src/invidious/routes/channels.cr @@ -216,8 +216,7 @@ module Invidious::Routes::Channels return error_template(400, "Invalid post ID") if response["error"]? ucid = response.dig("endpoint", "browseEndpoint", "browseId").as_s - params = response.dig("endpoint", "browseEndpoint", "params").as_s - post_response = fetch_channel_community_post(ucid, id, locale, "json", thin_mode, params: params) + post_response = fetch_channel_community_post(ucid, id, locale, "json", thin_mode) end post_response = JSON.parse(post_response) From bb04bcc42c1b135aaf50de8799264f86bc42f4db Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 29 Aug 2023 19:10:01 -0700 Subject: [PATCH 386/598] Apply suggestions from code review add videoId to resolve_url function Co-Authored-By: Samantaz Fox --- src/invidious/comments/youtube.cr | 4 ++-- src/invidious/routes/api/v1/channels.cr | 11 +++++++++-- src/invidious/routes/api/v1/misc.cr | 10 ++++------ src/invidious/routes/channels.cr | 2 -- src/invidious/views/post.ecr | 2 +- 5 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/invidious/comments/youtube.cr b/src/invidious/comments/youtube.cr index 01c2564f..185d8e43 100644 --- a/src/invidious/comments/youtube.cr +++ b/src/invidious/comments/youtube.cr @@ -37,14 +37,14 @@ module Invidious::Comments }, } - objectParsed = object.try { |i| Protodec::Any.cast_json(i) } + object_parsed = object.try { |i| Protodec::Any.cast_json(i) } .try { |i| Protodec::Any.from_json(i) } .try { |i| Base64.urlsafe_encode(i) } object2 = { "80226972:embedded" => { "2:string" => ucid, - "3:string" => objectParsed, + "3:string" => object_parsed, }, } diff --git a/src/invidious/routes/api/v1/channels.cr b/src/invidious/routes/api/v1/channels.cr index 0d2d2eb1..a5ae16a8 100644 --- a/src/invidious/routes/api/v1/channels.cr +++ b/src/invidious/routes/api/v1/channels.cr @@ -347,9 +347,8 @@ module Invidious::Routes::API::V1::Channels locale = env.get("preferences").as(Preferences).locale env.response.content_type = "application/json" - id = env.params.url["id"].to_s - ucid = env.params.query["ucid"] + ucid = env.params.query["ucid"]? thin_mode = env.params.query["thin_mode"]? thin_mode = thin_mode == "true" @@ -357,6 +356,14 @@ module Invidious::Routes::API::V1::Channels format = env.params.query["format"]? format ||= "json" + if ucid.nil? + response = YoutubeAPI.resolve_url("https://www.youtube.com/post/#{id}") + return error_json(400, "Invalid post ID") if response["error"]? + ucid = response.dig("endpoint", "browseEndpoint", "browseId").as_s + else + ucid = ucid.to_s + end + begin fetch_channel_community_post(ucid, id, locale, format, thin_mode) rescue ex diff --git a/src/invidious/routes/api/v1/misc.cr b/src/invidious/routes/api/v1/misc.cr index 6118a0d1..5dfc4afa 100644 --- a/src/invidious/routes/api/v1/misc.cr +++ b/src/invidious/routes/api/v1/misc.cr @@ -162,21 +162,19 @@ module Invidious::Routes::API::V1::Misc resolved_url = YoutubeAPI.resolve_url(url.as(String)) endpoint = resolved_url["endpoint"] pageType = endpoint.dig?("commandMetadata", "webCommandMetadata", "webPageType").try &.as_s || "" - if sub_endpoint = endpoint["watchEndpoint"]? - resolved_ucid = sub_endpoint["videoId"]? - elsif sub_endpoint = endpoint["browseEndpoint"]? - resolved_ucid = sub_endpoint["browseId"]? - elsif pageType == "WEB_PAGE_TYPE_UNKNOWN" + if pageType == "WEB_PAGE_TYPE_UNKNOWN" return error_json(400, "Unknown url") end + sub_endpoint = endpoint["watchEndpoint"]? || endpoint["browseEndpoint"]? || endpoint params = sub_endpoint.try &.dig?("params") rescue ex return error_json(500, ex) end JSON.build do |json| json.object do - json.field "ucid", resolved_ucid.try &.as_s || "" + json.field "ucid", sub_endpoint["browseId"].try &.as_s if sub_endpoint["browseId"]? + json.field "videoId", sub_endpoint["videoId"].try &.as_s if sub_endpoint["videoId"]? json.field "params", params.try &.as_s json.field "pageType", pageType end diff --git a/src/invidious/routes/channels.cr b/src/invidious/routes/channels.cr index 20b02dc1..29995bf6 100644 --- a/src/invidious/routes/channels.cr +++ b/src/invidious/routes/channels.cr @@ -205,8 +205,6 @@ module Invidious::Routes::Channels thin_mode = env.params.query["thin_mode"]? || prefs.thin_mode thin_mode = thin_mode == "true" - client_config = YoutubeAPI::ClientConfig.new(region: region) - if !ucid.nil? ucid = ucid.to_s post_response = fetch_channel_community_post(ucid, id, locale, "json", thin_mode) diff --git a/src/invidious/views/post.ecr b/src/invidious/views/post.ecr index b2cd778c..071d1c88 100644 --- a/src/invidious/views/post.ecr +++ b/src/invidious/views/post.ecr @@ -22,7 +22,7 @@ "comments": ["youtube"] }, "preferences" => prefs, - "base_url" => "/api/v1/post/" + id + "/comments", + "base_url" => "/api/v1/post/#{URI.encode_www_form(id)}/comments", "ucid" => ucid }.to_pretty_json %> From 8781520b8af221e5ab202775a1b58dd5e0e3fd83 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 18 Jul 2023 08:06:50 -0700 Subject: [PATCH 387/598] Search: Parse channel handle and hide video count when channel handle exists Co-Authored-By: Samantaz Fox --- src/invidious/helpers/serialized_yt_data.cr | 2 ++ src/invidious/views/components/item.ecr | 3 ++- src/invidious/yt_backend/extractors.cr | 10 ++++++---- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/invidious/helpers/serialized_yt_data.cr b/src/invidious/helpers/serialized_yt_data.cr index e0bd7279..31a3cf44 100644 --- a/src/invidious/helpers/serialized_yt_data.cr +++ b/src/invidious/helpers/serialized_yt_data.cr @@ -186,6 +186,7 @@ struct SearchChannel property author_thumbnail : String property subscriber_count : Int32 property video_count : Int32 + property channel_handle : String? property description_html : String property auto_generated : Bool property author_verified : Bool @@ -214,6 +215,7 @@ struct SearchChannel json.field "autoGenerated", self.auto_generated json.field "subCount", self.subscriber_count json.field "videoCount", self.video_count + json.field "channelHandle", self.channel_handle json.field "description", html_to_content(self.description_html) json.field "descriptionHtml", self.description_html diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr index c29ec47b..031b46da 100644 --- a/src/invidious/views/components/item.ecr +++ b/src/invidious/views/components/item.ecr @@ -26,8 +26,9 @@
    + <% if !item.channel_handle.nil? %>

    <%= item.channel_handle %>

    <% end %>

    <%= translate_count(locale, "generic_subscribers_count", item.subscriber_count, NumberFormatting::Separator) %>

    - <% if !item.auto_generated %>

    <%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %>

    <% end %> + <% if !item.auto_generated && item.channel_handle.nil? %>

    <%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %>

    <% end %>
    <%= item.description_html %>
    <% when SearchHashtag %> <% if !thin_mode %> diff --git a/src/invidious/yt_backend/extractors.cr b/src/invidious/yt_backend/extractors.cr index aaf7772e..56325cf7 100644 --- a/src/invidious/yt_backend/extractors.cr +++ b/src/invidious/yt_backend/extractors.cr @@ -175,17 +175,18 @@ private module Parsers # Always simpleText # TODO change default value to nil - subscriber_count = item_contents.dig?("subscriberCountText", "simpleText") + subscriber_count = item_contents.dig?("subscriberCountText", "simpleText").try &.as_s + channel_handle = subscriber_count if (subscriber_count.try &.starts_with? "@") # Since youtube added channel handles, `VideoCountText` holds the number of # subscribers and `subscriberCountText` holds the handle, except when the # channel doesn't have a handle (e.g: some topic music channels). # See https://github.com/iv-org/invidious/issues/3394#issuecomment-1321261688 - if !subscriber_count || !subscriber_count.as_s.includes? " subscriber" - subscriber_count = item_contents.dig?("videoCountText", "simpleText") + if !subscriber_count || !subscriber_count.includes? " subscriber" + subscriber_count = item_contents.dig?("videoCountText", "simpleText").try &.as_s end subscriber_count = subscriber_count - .try { |s| short_text_to_number(s.as_s.split(" ")[0]).to_i32 } || 0 + .try { |s| short_text_to_number(s.split(" ")[0]).to_i32 } || 0 # Auto-generated channels doesn't have videoCountText # Taken from: https://github.com/iv-org/invidious/pull/2228#discussion_r717620922 @@ -200,6 +201,7 @@ private module Parsers author_thumbnail: author_thumbnail, subscriber_count: subscriber_count, video_count: video_count, + channel_handle: channel_handle, description_html: description_html, auto_generated: auto_generated, author_verified: author_verified, From e8c9b85ef5b1eb933dffba0a2c5e03c12f03352e Mon Sep 17 00:00:00 2001 From: RadoslavL Date: Tue, 19 Sep 2023 09:15:44 +0300 Subject: [PATCH 388/598] Increased footer contrast --- assets/css/default.css | 49 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/assets/css/default.css b/assets/css/default.css index ec037240..5ddfd143 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -432,17 +432,30 @@ p.video-data { margin: 0; font-weight: bold; font-size: 80%; } * Footer */ -footer { - color: #919191; +.light-theme footer { + color: #7c7c7c; margin-top: auto; padding: 1.5em 0; text-align: center; max-height: 30vh; } -footer a { - color: #919191 !important; - text-decoration: underline; +.dark-theme footer { + color: #adadad; + margin-top: auto; + padding: 1.5em 0; + text-align: center; + max-height: 30vh; +} + +.light-theme footer a { + color: #7c7c7c !important; +/*text-decoration: underline;*/ +} + +.dark-theme footer a { + color: #adadad !important; +/*text-decoration: underline;*/ } footer span { @@ -548,6 +561,19 @@ span > select { color: #303030; } + .no-theme footer { + color: #7c7c7c; + margin-top: auto; + padding: 1.5em 0; + text-align: center; + max-height: 30vh; + } + + .no-theme footer a { + color: #7c7c7c !important; +/* text-decoration: underline;*/ + } + .light-theme .pure-menu-heading { color: #565d64; } @@ -666,6 +692,19 @@ body.dark-theme { background-color: inherit; color: inherit; } + + .no-theme footer { + color: #adadad; + margin-top: auto; + padding: 1.5em 0; + text-align: center; + max-height: 30vh; + } + + .no-theme footer a { + color: #adadad !important; + /*text-decoration: underline;*/ + } } From 54fa59cbb0ae90a54136522c944410e2d18c234b Mon Sep 17 00:00:00 2001 From: syeopite Date: Thu, 24 Aug 2023 14:58:50 -0700 Subject: [PATCH 389/598] Add method to construct WebVTT files Similar to JSON.Build --- spec/helpers/vtt/builder_spec.cr | 64 ++++++++++++++++++++++++++++++ src/invidious/helpers/webvtt.cr | 67 ++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 spec/helpers/vtt/builder_spec.cr create mode 100644 src/invidious/helpers/webvtt.cr diff --git a/spec/helpers/vtt/builder_spec.cr b/spec/helpers/vtt/builder_spec.cr new file mode 100644 index 00000000..69303bab --- /dev/null +++ b/spec/helpers/vtt/builder_spec.cr @@ -0,0 +1,64 @@ +require "../../spec_helper.cr" + +MockLines = [ + { + "start_time": Time::Span.new(seconds: 1), + "end_time": Time::Span.new(seconds: 2), + "text": "Line 1", + }, + + { + "start_time": Time::Span.new(seconds: 2), + "end_time": Time::Span.new(seconds: 3), + "text": "Line 2", + }, +] + +Spectator.describe "WebVTT::Builder" do + it "correctly builds a vtt file" do + result = WebVTT.build do |vtt| + MockLines.each do |line| + vtt.line(line["start_time"], line["end_time"], line["text"]) + end + end + + expect(result).to eq([ + "WEBVTT", + "", + "00:00:01.000 --> 00:00:02.000", + "Line 1", + "", + "00:00:02.000 --> 00:00:03.000", + "Line 2", + "", + "", + ].join('\n')) + end + + it "correctly builds a vtt file with setting fields" do + setting_fields = { + "Kind" => "captions", + "Language" => "en", + } + + result = WebVTT.build(setting_fields) do |vtt| + MockLines.each do |line| + vtt.line(line["start_time"], line["end_time"], line["text"]) + end + end + + expect(result).to eq([ + "WEBVTT", + "Kind: captions", + "Language: en", + "", + "00:00:01.000 --> 00:00:02.000", + "Line 1", + "", + "00:00:02.000 --> 00:00:03.000", + "Line 2", + "", + "", + ].join('\n')) + end +end diff --git a/src/invidious/helpers/webvtt.cr b/src/invidious/helpers/webvtt.cr new file mode 100644 index 00000000..7d9d5f1f --- /dev/null +++ b/src/invidious/helpers/webvtt.cr @@ -0,0 +1,67 @@ +# Namespace for logic relating to generating WebVTT files +# +# Probably not compliant to WebVTT's specs but it is enough for Invidious. +module WebVTT + # A WebVTT builder generates WebVTT files + private class Builder + def initialize(@io : IO) + end + + # Writes an vtt line with the specified time stamp and contents + def line(start_time : Time::Span, end_time : Time::Span, text : String) + timestamp(start_time, end_time) + @io << text + @io << "\n\n" + end + + private def timestamp(start_time : Time::Span, end_time : Time::Span) + add_timestamp_component(start_time) + @io << " --> " + add_timestamp_component(end_time) + + @io << '\n' + end + + private def add_timestamp_component(timestamp : Time::Span) + @io << timestamp.hours.to_s.rjust(2, '0') + @io << ':' << timestamp.minutes.to_s.rjust(2, '0') + @io << ':' << timestamp.seconds.to_s.rjust(2, '0') + @io << '.' << timestamp.milliseconds.to_s.rjust(3, '0') + end + + def document(setting_fields : Hash(String, String)? = nil, &) + @io << "WEBVTT\n" + + if setting_fields + setting_fields.each do |name, value| + @io << "#{name}: #{value}\n" + end + end + + @io << '\n' + + yield + end + end + + # Returns the resulting `String` of writing WebVTT to the yielded WebVTT::Builder + # + # ``` + # string = WebVTT.build do |io| + # vtt.line(Time::Span.new(seconds: 1), Time::Span.new(seconds: 2), "Line 1") + # vtt.line(Time::Span.new(seconds: 2), Time::Span.new(seconds: 3), "Line 2") + # end + # + # string # => "WEBVTT\n\n00:00:01.000 --> 00:00:02.000\nLine 1\n\n00:00:02.000 --> 00:00:03.000\nLine 2\n\n" + # ``` + # + # Accepts an optional settings fields hash to add settings attribute to the resulting vtt file. + def self.build(setting_fields : Hash(String, String)? = nil, &) + String.build do |str| + builder = Builder.new(str) + builder.document(setting_fields) do + yield builder + end + end + end +end From 0cb7d0b44137c2cee9b6352969a28dac4e3576c5 Mon Sep 17 00:00:00 2001 From: syeopite Date: Thu, 24 Aug 2023 15:10:50 -0700 Subject: [PATCH 390/598] Refactor Invidious's VTT logic to use WebVtt.build --- src/invidious/routes/api/v1/videos.cr | 39 +++++++------------------ src/invidious/videos/caption.cr | 41 ++++++++------------------- src/invidious/videos/transcript.cr | 40 +++++--------------------- 3 files changed, 29 insertions(+), 91 deletions(-) diff --git a/src/invidious/routes/api/v1/videos.cr b/src/invidious/routes/api/v1/videos.cr index 25e766d2..5c50a804 100644 --- a/src/invidious/routes/api/v1/videos.cr +++ b/src/invidious/routes/api/v1/videos.cr @@ -101,20 +101,17 @@ module Invidious::Routes::API::V1::Videos if caption.name.includes? "auto-generated" caption_xml = YT_POOL.client &.get(url).body + settings_field = { + "Kind" => "captions", + "Language" => "#{tlang || caption.language_code}", + } + if caption_xml.starts_with?("/, "") text = text.gsub(/<\/font>/, "") @@ -137,12 +131,7 @@ module Invidious::Routes::API::V1::Videos text = "#{md["text"]}" end - str << <<-END_CUE - #{start_time} --> #{end_time} - #{text} - - - END_CUE + webvtt.line(start_time, end_time, text) end end end @@ -215,11 +204,7 @@ module Invidious::Routes::API::V1::Videos storyboard = storyboard[0] end - String.build do |str| - str << <<-END_VTT - WEBVTT - END_VTT - + WebVTT.build do |vtt| start_time = 0.milliseconds end_time = storyboard[:interval].milliseconds @@ -231,12 +216,8 @@ module Invidious::Routes::API::V1::Videos storyboard[:storyboard_height].times do |j| storyboard[:storyboard_width].times do |k| - str << <<-END_CUE - #{start_time}.000 --> #{end_time}.000 - #{url}#xywh=#{storyboard[:width] * k},#{storyboard[:height] * j},#{storyboard[:width] - 2},#{storyboard[:height]} - - - END_CUE + current_cue_url = "#{url}#xywh=#{storyboard[:width] * k},#{storyboard[:height] * j},#{storyboard[:width] - 2},#{storyboard[:height]}" + vtt.line(start_time, end_time, current_cue_url) start_time += storyboard[:interval].milliseconds end_time += storyboard[:interval].milliseconds diff --git a/src/invidious/videos/caption.cr b/src/invidious/videos/caption.cr index 256dfcc0..dc58f9a0 100644 --- a/src/invidious/videos/caption.cr +++ b/src/invidious/videos/caption.cr @@ -52,17 +52,13 @@ module Invidious::Videos break end end - result = String.build do |result| - result << <<-END_VTT - WEBVTT - Kind: captions - Language: #{tlang || @language_code} + settings_field = { + "Kind" => "captions", + "Language" => "#{tlang || @language_code}", + } - END_VTT - - result << "\n\n" - + result = WebVTT.build(settings_field) do |vtt| cues.each_with_index do |node, i| start_time = node["t"].to_f.milliseconds @@ -76,29 +72,16 @@ module Invidious::Videos end_time = start_time + duration end - # start_time - result << start_time.hours.to_s.rjust(2, '0') - result << ':' << start_time.minutes.to_s.rjust(2, '0') - result << ':' << start_time.seconds.to_s.rjust(2, '0') - result << '.' << start_time.milliseconds.to_s.rjust(3, '0') - - result << " --> " - - # end_time - result << end_time.hours.to_s.rjust(2, '0') - result << ':' << end_time.minutes.to_s.rjust(2, '0') - result << ':' << end_time.seconds.to_s.rjust(2, '0') - result << '.' << end_time.milliseconds.to_s.rjust(3, '0') - - result << "\n" - - node.children.each do |s| - result << s.content + text = String.build do |io| + node.children.each do |s| + io << s.content + end end - result << "\n" - result << "\n" + + vtt.line(start_time, end_time, text) end end + return result end end diff --git a/src/invidious/videos/transcript.cr b/src/invidious/videos/transcript.cr index f3360a52..cd97cfde 100644 --- a/src/invidious/videos/transcript.cr +++ b/src/invidious/videos/transcript.cr @@ -34,41 +34,15 @@ module Invidious::Videos # Convert into array of TranscriptLine lines = self.parse(initial_data) + settings_field = { + "Kind" => "captions", + "Language" => target_language + } + # Taken from Invidious::Videos::Captions::Metadata.timedtext_to_vtt() - vtt = String.build do |vtt| - vtt << <<-END_VTT - WEBVTT - Kind: captions - Language: #{target_language} - - - END_VTT - - vtt << "\n\n" - + vtt = WebVTT.build(settings_field) do |vtt| lines.each do |line| - start_time = line.start_ms - end_time = line.end_ms - - # start_time - vtt << start_time.hours.to_s.rjust(2, '0') - vtt << ':' << start_time.minutes.to_s.rjust(2, '0') - vtt << ':' << start_time.seconds.to_s.rjust(2, '0') - vtt << '.' << start_time.milliseconds.to_s.rjust(3, '0') - - vtt << " --> " - - # end_time - vtt << end_time.hours.to_s.rjust(2, '0') - vtt << ':' << end_time.minutes.to_s.rjust(2, '0') - vtt << ':' << end_time.seconds.to_s.rjust(2, '0') - vtt << '.' << end_time.milliseconds.to_s.rjust(3, '0') - - vtt << "\n" - vtt << line.line - - vtt << "\n" - vtt << "\n" + vtt.line(line.start_ms, line.end_ms, line.line) end end From d371eb50f27b9d29bc68ec883d8bee54894c79a4 Mon Sep 17 00:00:00 2001 From: syeopite Date: Thu, 24 Aug 2023 15:42:42 -0700 Subject: [PATCH 391/598] WebVTT::Builder: rename #line to #cue --- spec/helpers/vtt/builder_spec.cr | 4 ++-- src/invidious/helpers/webvtt.cr | 8 ++++---- src/invidious/routes/api/v1/videos.cr | 4 ++-- src/invidious/videos/caption.cr | 2 +- src/invidious/videos/transcript.cr | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/spec/helpers/vtt/builder_spec.cr b/spec/helpers/vtt/builder_spec.cr index 69303bab..7b543ddc 100644 --- a/spec/helpers/vtt/builder_spec.cr +++ b/spec/helpers/vtt/builder_spec.cr @@ -18,7 +18,7 @@ Spectator.describe "WebVTT::Builder" do it "correctly builds a vtt file" do result = WebVTT.build do |vtt| MockLines.each do |line| - vtt.line(line["start_time"], line["end_time"], line["text"]) + vtt.cue(line["start_time"], line["end_time"], line["text"]) end end @@ -43,7 +43,7 @@ Spectator.describe "WebVTT::Builder" do result = WebVTT.build(setting_fields) do |vtt| MockLines.each do |line| - vtt.line(line["start_time"], line["end_time"], line["text"]) + vtt.cue(line["start_time"], line["end_time"], line["text"]) end end diff --git a/src/invidious/helpers/webvtt.cr b/src/invidious/helpers/webvtt.cr index 7d9d5f1f..c50d7fa2 100644 --- a/src/invidious/helpers/webvtt.cr +++ b/src/invidious/helpers/webvtt.cr @@ -7,8 +7,8 @@ module WebVTT def initialize(@io : IO) end - # Writes an vtt line with the specified time stamp and contents - def line(start_time : Time::Span, end_time : Time::Span, text : String) + # Writes an vtt cue with the specified time stamp and contents + def cue(start_time : Time::Span, end_time : Time::Span, text : String) timestamp(start_time, end_time) @io << text @io << "\n\n" @@ -48,8 +48,8 @@ module WebVTT # # ``` # string = WebVTT.build do |io| - # vtt.line(Time::Span.new(seconds: 1), Time::Span.new(seconds: 2), "Line 1") - # vtt.line(Time::Span.new(seconds: 2), Time::Span.new(seconds: 3), "Line 2") + # vtt.cue(Time::Span.new(seconds: 1), Time::Span.new(seconds: 2), "Line 1") + # vtt.cue(Time::Span.new(seconds: 2), Time::Span.new(seconds: 3), "Line 2") # end # # string # => "WEBVTT\n\n00:00:01.000 --> 00:00:02.000\nLine 1\n\n00:00:02.000 --> 00:00:03.000\nLine 2\n\n" diff --git a/src/invidious/routes/api/v1/videos.cr b/src/invidious/routes/api/v1/videos.cr index 5c50a804..449c9f9b 100644 --- a/src/invidious/routes/api/v1/videos.cr +++ b/src/invidious/routes/api/v1/videos.cr @@ -131,7 +131,7 @@ module Invidious::Routes::API::V1::Videos text = "#{md["text"]}" end - webvtt.line(start_time, end_time, text) + webvtt.cue(start_time, end_time, text) end end end @@ -217,7 +217,7 @@ module Invidious::Routes::API::V1::Videos storyboard[:storyboard_height].times do |j| storyboard[:storyboard_width].times do |k| current_cue_url = "#{url}#xywh=#{storyboard[:width] * k},#{storyboard[:height] * j},#{storyboard[:width] - 2},#{storyboard[:height]}" - vtt.line(start_time, end_time, current_cue_url) + vtt.cue(start_time, end_time, current_cue_url) start_time += storyboard[:interval].milliseconds end_time += storyboard[:interval].milliseconds diff --git a/src/invidious/videos/caption.cr b/src/invidious/videos/caption.cr index dc58f9a0..484e61d2 100644 --- a/src/invidious/videos/caption.cr +++ b/src/invidious/videos/caption.cr @@ -78,7 +78,7 @@ module Invidious::Videos end end - vtt.line(start_time, end_time, text) + vtt.cue(start_time, end_time, text) end end diff --git a/src/invidious/videos/transcript.cr b/src/invidious/videos/transcript.cr index cd97cfde..055d96fb 100644 --- a/src/invidious/videos/transcript.cr +++ b/src/invidious/videos/transcript.cr @@ -42,7 +42,7 @@ module Invidious::Videos # Taken from Invidious::Videos::Captions::Metadata.timedtext_to_vtt() vtt = WebVTT.build(settings_field) do |vtt| lines.each do |line| - vtt.line(line.start_ms, line.end_ms, line.line) + vtt.cue(line.start_ms, line.end_ms, line.line) end end From 4e97d8ad0942bd64a23ed4a2ba89e48a97c520aa Mon Sep 17 00:00:00 2001 From: syeopite Date: Thu, 24 Aug 2023 16:27:06 -0700 Subject: [PATCH 392/598] Update documentation for `WebVTT.build` --- src/invidious/helpers/webvtt.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/invidious/helpers/webvtt.cr b/src/invidious/helpers/webvtt.cr index c50d7fa2..52138854 100644 --- a/src/invidious/helpers/webvtt.cr +++ b/src/invidious/helpers/webvtt.cr @@ -44,10 +44,10 @@ module WebVTT end end - # Returns the resulting `String` of writing WebVTT to the yielded WebVTT::Builder + # Returns the resulting `String` of writing WebVTT to the yielded `WebVTT::Builder` # # ``` - # string = WebVTT.build do |io| + # string = WebVTT.build do |vtt| # vtt.cue(Time::Span.new(seconds: 1), Time::Span.new(seconds: 2), "Line 1") # vtt.cue(Time::Span.new(seconds: 2), Time::Span.new(seconds: 3), "Line 2") # end From e9d59a6dfd14fd115f3bfc59ca6f33182a631575 Mon Sep 17 00:00:00 2001 From: syeopite <70992037+syeopite@users.noreply.github.com> Date: Tue, 29 Aug 2023 05:59:08 +0000 Subject: [PATCH 393/598] Update src/invidious/helpers/webvtt.cr Co-authored-by: Samantaz Fox --- src/invidious/helpers/webvtt.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/helpers/webvtt.cr b/src/invidious/helpers/webvtt.cr index 52138854..aace6bb8 100644 --- a/src/invidious/helpers/webvtt.cr +++ b/src/invidious/helpers/webvtt.cr @@ -34,7 +34,7 @@ module WebVTT if setting_fields setting_fields.each do |name, value| - @io << "#{name}: #{value}\n" + @io << name << ": " << value << '\n' end end From a999438ae46739477a6ca5f8515fa70b6b492443 Mon Sep 17 00:00:00 2001 From: syeopite Date: Mon, 28 Aug 2023 23:14:25 -0700 Subject: [PATCH 394/598] Consistency: rename #add_timestamp_component Removes the add_ prefix for consistency with the other methods in WebVTT::Builder --- src/invidious/helpers/webvtt.cr | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/invidious/helpers/webvtt.cr b/src/invidious/helpers/webvtt.cr index aace6bb8..56f761ed 100644 --- a/src/invidious/helpers/webvtt.cr +++ b/src/invidious/helpers/webvtt.cr @@ -15,14 +15,14 @@ module WebVTT end private def timestamp(start_time : Time::Span, end_time : Time::Span) - add_timestamp_component(start_time) + timestamp_component(start_time) @io << " --> " - add_timestamp_component(end_time) + timestamp_component(end_time) @io << '\n' end - private def add_timestamp_component(timestamp : Time::Span) + private def timestamp_component(timestamp : Time::Span) @io << timestamp.hours.to_s.rjust(2, '0') @io << ':' << timestamp.minutes.to_s.rjust(2, '0') @io << ':' << timestamp.seconds.to_s.rjust(2, '0') From be2feba17c2f3b9d8e043825beff57568df46f2e Mon Sep 17 00:00:00 2001 From: syeopite Date: Sat, 23 Sep 2023 09:57:26 -0400 Subject: [PATCH 395/598] Lint --- src/invidious/videos/transcript.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/invidious/videos/transcript.cr b/src/invidious/videos/transcript.cr index 055d96fb..dac00eea 100644 --- a/src/invidious/videos/transcript.cr +++ b/src/invidious/videos/transcript.cr @@ -35,8 +35,8 @@ module Invidious::Videos lines = self.parse(initial_data) settings_field = { - "Kind" => "captions", - "Language" => target_language + "Kind" => "captions", + "Language" => target_language, } # Taken from Invidious::Videos::Captions::Metadata.timedtext_to_vtt() From ea781ceeeebbf052c377cf3dacec416e9ac25453 Mon Sep 17 00:00:00 2001 From: RadoslavL Date: Sun, 24 Sep 2023 10:08:16 +0300 Subject: [PATCH 396/598] Removed unnecessary lines --- assets/css/default.css | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/assets/css/default.css b/assets/css/default.css index 5ddfd143..720b807c 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -432,20 +432,19 @@ p.video-data { margin: 0; font-weight: bold; font-size: 80%; } * Footer */ -.light-theme footer { - color: #7c7c7c; +footer { margin-top: auto; padding: 1.5em 0; text-align: center; max-height: 30vh; } +.light-theme footer { + color: #7c7c7c; +} + .dark-theme footer { color: #adadad; - margin-top: auto; - padding: 1.5em 0; - text-align: center; - max-height: 30vh; } .light-theme footer a { @@ -563,10 +562,6 @@ span > select { .no-theme footer { color: #7c7c7c; - margin-top: auto; - padding: 1.5em 0; - text-align: center; - max-height: 30vh; } .no-theme footer a { @@ -695,10 +690,6 @@ body.dark-theme { .no-theme footer { color: #adadad; - margin-top: auto; - padding: 1.5em 0; - text-align: center; - max-height: 30vh; } .no-theme footer a { From 47cc9dc169595af77f4fdd740d83479d5d111f43 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Wed, 27 Sep 2023 23:01:25 +0200 Subject: [PATCH 397/598] JS: Fix missing domain in URL constructor --- assets/js/player.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/assets/js/player.js b/assets/js/player.js index bed02875..c34da9b5 100644 --- a/assets/js/player.js +++ b/assets/js/player.js @@ -98,11 +98,13 @@ if (video_data.params.quality === 'dash') { /** * Function for add time argument to url + * * @param {String} url + * @param {String} [base] * @returns {URL} urlWithTimeArg */ -function addCurrentTimeToURL(url) { - var urlUsed = new URL(url); +function addCurrentTimeToURL(url, base) { + var urlUsed = new URL(url, base); urlUsed.searchParams.delete('start'); var currentTime = Math.ceil(player.currentTime()); if (currentTime > 0) @@ -132,14 +134,16 @@ player.on('timeupdate', function () { // Invidious links + let domain = window.location.origin; + let elem_iv_embed = document.getElementById('link-iv-embed'); let elem_iv_other = document.getElementById('link-iv-other'); let base_url_iv_embed = elem_iv_embed.getAttribute('data-base-url'); let base_url_iv_other = elem_iv_other.getAttribute('data-base-url'); - elem_iv_embed.href = addCurrentTimeToURL(base_url_iv_embed); - elem_iv_other.href = addCurrentTimeToURL(base_url_iv_other); + elem_iv_embed.href = addCurrentTimeToURL(base_url_iv_embed, domain); + elem_iv_other.href = addCurrentTimeToURL(base_url_iv_other, domain); }); From 4f25069f55b5ee87bb214a975d97522155bffc2c Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 26 Sep 2023 19:05:37 -0400 Subject: [PATCH 398/598] remove unused variable simplify resolve url remove trailing spaces Co-Authored-By: Samantaz Fox --- assets/js/comments.js | 2 +- src/invidious/routes/api/v1/channels.cr | 1 - src/invidious/routes/api/v1/misc.cr | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/assets/js/comments.js b/assets/js/comments.js index 00a8cae9..35ffa96e 100644 --- a/assets/js/comments.js +++ b/assets/js/comments.js @@ -131,7 +131,7 @@ function get_youtube_replies(target, load_more, load_replies) { '&hl=' + video_data.preferences.locale + '&thin_mode=' + video_data.preferences.thin_mode + '&continuation=' + continuation; - + if (video_data.ucid) { url += '&ucid=' + video_data.ucid } diff --git a/src/invidious/routes/api/v1/channels.cr b/src/invidious/routes/api/v1/channels.cr index a5ae16a8..67018660 100644 --- a/src/invidious/routes/api/v1/channels.cr +++ b/src/invidious/routes/api/v1/channels.cr @@ -373,7 +373,6 @@ module Invidious::Routes::API::V1::Channels def self.post_comments(env) locale = env.get("preferences").as(Preferences).locale - region = env.params.query["region"]? env.response.content_type = "application/json" diff --git a/src/invidious/routes/api/v1/misc.cr b/src/invidious/routes/api/v1/misc.cr index 5dfc4afa..8a92e160 100644 --- a/src/invidious/routes/api/v1/misc.cr +++ b/src/invidious/routes/api/v1/misc.cr @@ -173,8 +173,8 @@ module Invidious::Routes::API::V1::Misc end JSON.build do |json| json.object do - json.field "ucid", sub_endpoint["browseId"].try &.as_s if sub_endpoint["browseId"]? - json.field "videoId", sub_endpoint["videoId"].try &.as_s if sub_endpoint["videoId"]? + json.field "ucid", sub_endpoint["browseId"].as_s if sub_endpoint["browseId"]? + json.field "videoId", sub_endpoint["videoId"].as_s if sub_endpoint["videoId"]? json.field "params", params.try &.as_s json.field "pageType", pageType end From f77e4378fe1ee69d0cf8adced1c8eef8140896ee Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Wed, 27 Sep 2023 18:40:50 -0400 Subject: [PATCH 399/598] Add support for viewing comments without js Improve stylings --- assets/css/default.css | 8 ++++++++ src/invidious/routes/channels.cr | 11 ++++++++++- src/invidious/views/post.ecr | 25 +++++++++++++++++++++---- 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/assets/css/default.css b/assets/css/default.css index 69fe8d5f..b4053b5c 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -397,6 +397,14 @@ p.video-data { margin: 0; font-weight: bold; font-size: 80%; } margin: auto; } +/* + * We don't want the top and bottom margin on the post page. + */ +.comments.post-comments { + margin-bottom: 0; + margin-top: 0; +} + .video-iframe-wrapper { position: relative; height: 0; diff --git a/src/invidious/routes/channels.cr b/src/invidious/routes/channels.cr index 29995bf6..62b3884e 100644 --- a/src/invidious/routes/channels.cr +++ b/src/invidious/routes/channels.cr @@ -200,11 +200,15 @@ module Invidious::Routes::Channels prefs = env.get("preferences").as(Preferences) locale = prefs.locale - region = env.params.query["region"]? || prefs.region thin_mode = env.params.query["thin_mode"]? || prefs.thin_mode thin_mode = thin_mode == "true" + nojs = env.params.query["nojs"]? + + nojs ||= "0" + nojs = nojs == "1" + if !ucid.nil? ucid = ucid.to_s post_response = fetch_channel_community_post(ucid, id, locale, "json", thin_mode) @@ -218,6 +222,11 @@ module Invidious::Routes::Channels end post_response = JSON.parse(post_response) + + if nojs + comments = Comments.fetch_community_post_comments(ucid, id) + comment_html = JSON.parse(Comments.parse_youtube(id, comments, "html", locale, thin_mode, isPost: true))["contentHtml"] + end templated "post" end diff --git a/src/invidious/views/post.ecr b/src/invidious/views/post.ecr index 071d1c88..fb03a44c 100644 --- a/src/invidious/views/post.ecr +++ b/src/invidious/views/post.ecr @@ -2,10 +2,27 @@ Invidious <% end %> -
    - <%= IV::Frontend::Comments.template_youtube(post_response.not_nil!, locale, thin_mode) %> -
    -
    +
    +
    + <%= IV::Frontend::Comments.template_youtube(post_response.not_nil!, locale, thin_mode) %> +
    + + <% if nojs %> +
    + <% end %> +
    + +
    + <% if nojs %> + <%= comment_html %> + <% else %> + + <% end %> +
    -<% - locale = env.get("preferences").as(Preferences).locale - dark_mode = env.get("preferences").as(Preferences).dark_mode -%> - -theme"> - +
    @@ -43,8 +42,8 @@
    <% if env.get? "user" %> <% else %>
    - " class="pure-menu-heading"> - <% if env.get("preferences").as(Preferences).dark_mode == "dark" %> + " class="pure-menu-heading" title="<%= translate(locale, "toggle_theme") %>"> + <% if dark_mode == "dark" %> <% else %> From 87a8207f370f20582d6330d9fcf4346fbb4e1ae5 Mon Sep 17 00:00:00 2001 From: guidiasz Date: Mon, 18 Dec 2023 13:23:55 -0300 Subject: [PATCH 490/598] fix: "Watch on YouTube" preserve current playlist --- src/invidious/views/watch.ecr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 07474896..cce6115a 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -118,7 +118,7 @@ we're going to need to do it here in order to allow for translations. link_yt_embed = URI.new(scheme: "https", host: "www.youtube.com", path: "/embed/#{video.id}") if !plid.nil? && !continuation.nil? - link_yt_param = URI::Params{"plid" => [plid], "index" => [continuation.to_s]} + link_yt_param = URI::Params{"list" => [plid], "index" => [continuation.to_s]} link_yt_watch = IV::HttpServer::Utils.add_params_to_url(link_yt_watch, link_yt_param) link_yt_embed = IV::HttpServer::Utils.add_params_to_url(link_yt_embed, link_yt_param) end From 97c4165f55c4574efb554c9dae8d919d08da1cdd Mon Sep 17 00:00:00 2001 From: Luigi Date: Mon, 18 Dec 2023 23:18:05 +0100 Subject: [PATCH 491/598] Improve depends_on docker-compose (#4249) * Improve depends_on checking the service is up and healthy before start the service that might cause issue first boot * Docker version Ubuntu 22.04 has a version which doesn't support restart --- docker-compose.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index d879919a..42a5c06b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -37,7 +37,8 @@ services: timeout: 5s retries: 2 depends_on: - - invidious-db + invidious-db: + condition: service_healthy invidious-db: image: docker.io/library/postgres:14 From 090b470bfcadce192439500ff89598fc6ba3faac Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 19 Dec 2023 23:07:18 -0500 Subject: [PATCH 492/598] fix potential memory leak --- src/invidious/routes/api/v1/search.cr | 1 + 1 file changed, 1 insertion(+) diff --git a/src/invidious/routes/api/v1/search.cr b/src/invidious/routes/api/v1/search.cr index a65571ea..2922b060 100644 --- a/src/invidious/routes/api/v1/search.cr +++ b/src/invidious/routes/api/v1/search.cr @@ -37,6 +37,7 @@ module Invidious::Routes::API::V1::Search url = "/complete/search?client=youtube&hl=en&gl=#{region}&q=#{URI.encode_www_form(query)}&gs_ri=youtube&ds=yt" response = client.get(url).body + client.close body = JSON.parse(response[19..-2]).as_a suggestions = body[1].as_a[0..-2] From 0917efd9cbf4129d508217dbf38c98db5eba13cf Mon Sep 17 00:00:00 2001 From: nixos script Date: Thu, 21 Dec 2023 13:50:32 +0800 Subject: [PATCH 493/598] fix issue where scope would be missing the * if the user was not logged in before calling the authorize endpoint fix #4200 --- src/invidious/helpers/utils.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index a006d602..e438e3b9 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -262,7 +262,7 @@ def get_referer(env, fallback = "/", unroll = true) end referer = referer.request_target - referer = "/" + referer.gsub(/[^\/?@&%=\-_.:,0-9a-zA-Z]/, "").lstrip("/\\") + referer = "/" + referer.gsub(/[^\/?@&%=\-_.:,*0-9a-zA-Z]/, "").lstrip("/\\") if referer == env.request.path referer = fallback From 7da4a7f72b0e328f72aff884605a21c4ffe7cb04 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 19 Dec 2023 22:37:48 -0500 Subject: [PATCH 494/598] add null safety to clip parsing --- src/invidious/routes/watch.cr | 4 ++-- src/invidious/videos/clip.cr | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/invidious/routes/watch.cr b/src/invidious/routes/watch.cr index 1cba86f6..aabe8dfc 100644 --- a/src/invidious/routes/watch.cr +++ b/src/invidious/routes/watch.cr @@ -277,8 +277,8 @@ module Invidious::Routes::Watch if video_id = response.dig?("endpoint", "watchEndpoint", "videoId") if params = response.dig?("endpoint", "watchEndpoint", "params").try &.as_s start_time, end_time, _ = parse_clip_parameters(params) - env.params.query["start"] = start_time.to_s - env.params.query["end"] = end_time.to_s + env.params.query["start"] = start_time.to_s if start_time != nil + env.params.query["end"] = end_time.to_s if end_time != nil end return env.redirect "/watch?v=#{video_id}&#{env.params.query}" diff --git a/src/invidious/videos/clip.cr b/src/invidious/videos/clip.cr index 47f108a3..29c57182 100644 --- a/src/invidious/videos/clip.cr +++ b/src/invidious/videos/clip.cr @@ -1,7 +1,7 @@ require "json" # returns start_time, end_time and clip_title -def parse_clip_parameters(params) : {Float64, Float64, String} +def parse_clip_parameters(params) : {Float64?, Float64?, String?} decoded_protobuf = params.try { |i| URI.decode_www_form(i) } .try { |i| Base64.decode(i) } .try { |i| IO::Memory.new(i) } @@ -9,12 +9,14 @@ def parse_clip_parameters(params) : {Float64, Float64, String} start_time = decoded_protobuf .try(&.["50:0:embedded"]["2:1:varint"].as_i64) + .try { |i| i/1000 } end_time = decoded_protobuf .try(&.["50:0:embedded"]["3:2:varint"].as_i64) + .try { |i| i/1000 } clip_title = decoded_protobuf .try(&.["50:0:embedded"]["4:3:string"].as_s) - return (start_time / 1000), (end_time / 1000), clip_title + return start_time, end_time, clip_title end From c059829035855089414495c00b5212d63737b4b1 Mon Sep 17 00:00:00 2001 From: pitkajuh Date: Fri, 5 Jan 2024 20:39:29 +0100 Subject: [PATCH 495/598] Fix typo --- locales/fi.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/fi.json b/locales/fi.json index 5d8578a5..14c2b0fc 100644 --- a/locales/fi.json +++ b/locales/fi.json @@ -14,7 +14,7 @@ "Clear watch history?": "Tyhjennä katseluhistoria?", "New password": "Uusi salasana", "New passwords must match": "Uusien salasanojen täytyy täsmätä", - "Authorize token?": "Valuutetaanko tunnus?", + "Authorize token?": "Valtuutetaanko tunnus?", "Authorize token for `x`?": "Valtuutetaanko tunnus `x`:lle?", "Yes": "Kyllä", "No": "Ei", From 7cca1285aaa1463eb31f82e49f903b437b4de69f Mon Sep 17 00:00:00 2001 From: vojkovic Date: Sat, 6 Jan 2024 15:51:31 +0800 Subject: [PATCH 496/598] Fix two swapped function names --- src/invidious/database/statistics.cr | 4 ++-- src/invidious/jobs/statistics_refresh_job.cr | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/invidious/database/statistics.cr b/src/invidious/database/statistics.cr index 1df549e2..9e4963fd 100644 --- a/src/invidious/database/statistics.cr +++ b/src/invidious/database/statistics.cr @@ -15,7 +15,7 @@ module Invidious::Database::Statistics PG_DB.query_one(request, as: Int64) end - def count_users_active_1m : Int64 + def count_users_active_6m : Int64 request = <<-SQL SELECT count(*) FROM users WHERE CURRENT_TIMESTAMP - updated < '6 months' @@ -24,7 +24,7 @@ module Invidious::Database::Statistics PG_DB.query_one(request, as: Int64) end - def count_users_active_6m : Int64 + def count_users_active_1m : Int64 request = <<-SQL SELECT count(*) FROM users WHERE CURRENT_TIMESTAMP - updated < '1 month' diff --git a/src/invidious/jobs/statistics_refresh_job.cr b/src/invidious/jobs/statistics_refresh_job.cr index 72d1ce88..66c91ad5 100644 --- a/src/invidious/jobs/statistics_refresh_job.cr +++ b/src/invidious/jobs/statistics_refresh_job.cr @@ -56,8 +56,8 @@ class Invidious::Jobs::StatisticsRefreshJob < Invidious::Jobs::BaseJob users = STATISTICS.dig("usage", "users").as(Hash(String, Int64)) users["total"] = Invidious::Database::Statistics.count_users_total - users["activeHalfyear"] = Invidious::Database::Statistics.count_users_active_1m - users["activeMonth"] = Invidious::Database::Statistics.count_users_active_6m + users["activeHalfyear"] = Invidious::Database::Statistics.count_users_active_6m + users["activeMonth"] = Invidious::Database::Statistics.count_users_active_1m STATISTICS["metadata"] = { "updatedAt" => Time.utc.to_unix, From b16f66ef0003843c4561f7bb3124339e6b446b2b Mon Sep 17 00:00:00 2001 From: syeopite <70992037+syeopite@users.noreply.github.com> Date: Wed, 10 Jan 2024 20:40:19 +0000 Subject: [PATCH 497/598] Exempt issues with "exempt-stale" from staling (#4385) The exempt-stale label was not actually set to exempt issues from staling... --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index b25199e3..16d3269b 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -16,7 +16,7 @@ jobs: days-before-stale: 365 days-before-pr-stale: 90 days-before-close: 30 - exempt-pr-labels: blocked + exempt-pr-labels: blocked,exempt-stale stale-issue-message: 'This issue has been automatically marked as stale and will be closed in 30 days because it has not had recent activity and is much likely outdated. If you think this issue is still relevant and applicable, you just have to post a comment and it will be unmarked.' stale-pr-message: 'This pull request has been automatically marked as stale and will be closed in 30 days because it has not had recent activity and is much likely abandoned or outdated. If you think this pull request is still relevant and applicable, you just have to post a comment and it will be unmarked.' stale-issue-label: "stale" From 1c0b4205d40781ff2d34d64dddf29e5dc89d1723 Mon Sep 17 00:00:00 2001 From: syeopite <70992037+syeopite@users.noreply.github.com> Date: Wed, 10 Jan 2024 23:01:00 +0000 Subject: [PATCH 498/598] Add parameter to disable `force_resolve` in `make_client` (#4335) * Add option to disable force_resolve in make_client Some websites such as archive.org and textcaptcha.com does not support IPv6 and as such requests fail when Invidious requests with IPv6 to those services. * Reenable force_resolve on pubsub subcribe request * Make force_resolve false by default in make_client * Remove missed explicit force_resolve=false --- src/invidious/routes/video_playback.cr | 8 ++++---- src/invidious/yt_backend/connection_pool.cr | 15 ++++++++++----- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/invidious/routes/video_playback.cr b/src/invidious/routes/video_playback.cr index 1d5aa914..ec18f3b8 100644 --- a/src/invidious/routes/video_playback.cr +++ b/src/invidious/routes/video_playback.cr @@ -42,7 +42,7 @@ module Invidious::Routes::VideoPlayback headers["Range"] = "bytes=#{range_for_head}" end - client = make_client(URI.parse(host), region) + client = make_client(URI.parse(host), region, force_resolve = true) response = HTTP::Client::Response.new(500) error = "" 5.times do @@ -57,7 +57,7 @@ module Invidious::Routes::VideoPlayback if new_host != host host = new_host client.close - client = make_client(URI.parse(new_host), region) + client = make_client(URI.parse(new_host), region, force_resolve = true) end url = "#{location.request_target}&host=#{location.host}#{region ? "®ion=#{region}" : ""}" @@ -71,7 +71,7 @@ module Invidious::Routes::VideoPlayback fvip = "3" host = "https://r#{fvip}---#{mn}.googlevideo.com" - client = make_client(URI.parse(host), region) + client = make_client(URI.parse(host), region, force_resolve = true) rescue ex error = ex.message end @@ -196,7 +196,7 @@ module Invidious::Routes::VideoPlayback break else client.close - client = make_client(URI.parse(host), region) + client = make_client(URI.parse(host), region, force_resolve = true) end end diff --git a/src/invidious/yt_backend/connection_pool.cr b/src/invidious/yt_backend/connection_pool.cr index 36e82766..81cfb272 100644 --- a/src/invidious/yt_backend/connection_pool.cr +++ b/src/invidious/yt_backend/connection_pool.cr @@ -26,7 +26,7 @@ struct YoutubeConnectionPool def client(region = nil, &block) if region - conn = make_client(url, region) + conn = make_client(url, region, force_resolve = true) response = yield conn else conn = pool.checkout @@ -59,9 +59,14 @@ struct YoutubeConnectionPool end end -def make_client(url : URI, region = nil) +def make_client(url : URI, region = nil, force_resolve : Bool = false) client = HTTPClient.new(url, OpenSSL::SSL::Context::Client.insecure) - client.family = CONFIG.force_resolve + + # Some services do not support IPv6. + if force_resolve + client.family = CONFIG.force_resolve + end + client.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com" client.read_timeout = 10.seconds client.connect_timeout = 10.seconds @@ -80,8 +85,8 @@ def make_client(url : URI, region = nil) return client end -def make_client(url : URI, region = nil, &block) - client = make_client(url, region) +def make_client(url : URI, region = nil, force_resolve : Bool = false, &block) + client = make_client(url, region, force_resolve) begin yield client ensure From 4a339df5c49e30a5ef5008d26639eb69edfff152 Mon Sep 17 00:00:00 2001 From: toabr <25079664+toabr@users.noreply.github.com> Date: Sat, 27 Jan 2024 00:38:47 +0100 Subject: [PATCH 499/598] CSS: expand #contents width on small screens --- assets/css/default.css | 1 + src/invidious/views/template.ecr | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/assets/css/default.css b/assets/css/default.css index 00881253..fd696178 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -13,6 +13,7 @@ body { display: flex; flex-direction: column; min-height: 100vh; + margin: auto; } .h-box { diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr index 77265679..5e2cf88e 100644 --- a/src/invidious/views/template.ecr +++ b/src/invidious/views/template.ecr @@ -28,8 +28,7 @@ -theme">
    -
    -
    +
    From c005ada48723808e507d0a4d5a3363a1c14a4f07 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Mon, 29 Jan 2024 14:59:25 +0100 Subject: [PATCH 500/598] fix: prevent censoring of self-harm related search queries (#4403) * fix: prevent censoring of self-harm related search queries * fix: yt_filters_spec with new flag --- spec/invidious/search/yt_filters_spec.cr | 54 ++++++++++++------------ src/invidious/search/filters.cr | 6 +-- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/spec/invidious/search/yt_filters_spec.cr b/spec/invidious/search/yt_filters_spec.cr index bf7f21e7..8abed5ce 100644 --- a/spec/invidious/search/yt_filters_spec.cr +++ b/spec/invidious/search/yt_filters_spec.cr @@ -12,45 +12,45 @@ end # page of Youtube with any browser devtools HTML inspector. DATE_FILTERS = { - Invidious::Search::Filters::Date::Hour => "EgIIAQ%3D%3D", - Invidious::Search::Filters::Date::Today => "EgIIAg%3D%3D", - Invidious::Search::Filters::Date::Week => "EgIIAw%3D%3D", - Invidious::Search::Filters::Date::Month => "EgIIBA%3D%3D", - Invidious::Search::Filters::Date::Year => "EgIIBQ%3D%3D", + Invidious::Search::Filters::Date::Hour => "EgIIAfABAQ%3D%3D", + Invidious::Search::Filters::Date::Today => "EgIIAvABAQ%3D%3D", + Invidious::Search::Filters::Date::Week => "EgIIA_ABAQ%3D%3D", + Invidious::Search::Filters::Date::Month => "EgIIBPABAQ%3D%3D", + Invidious::Search::Filters::Date::Year => "EgIIBfABAQ%3D%3D", } TYPE_FILTERS = { - Invidious::Search::Filters::Type::Video => "EgIQAQ%3D%3D", - Invidious::Search::Filters::Type::Channel => "EgIQAg%3D%3D", - Invidious::Search::Filters::Type::Playlist => "EgIQAw%3D%3D", - Invidious::Search::Filters::Type::Movie => "EgIQBA%3D%3D", + Invidious::Search::Filters::Type::Video => "EgIQAfABAQ%3D%3D", + Invidious::Search::Filters::Type::Channel => "EgIQAvABAQ%3D%3D", + Invidious::Search::Filters::Type::Playlist => "EgIQA_ABAQ%3D%3D", + Invidious::Search::Filters::Type::Movie => "EgIQBPABAQ%3D%3D", } DURATION_FILTERS = { - Invidious::Search::Filters::Duration::Short => "EgIYAQ%3D%3D", - Invidious::Search::Filters::Duration::Medium => "EgIYAw%3D%3D", - Invidious::Search::Filters::Duration::Long => "EgIYAg%3D%3D", + Invidious::Search::Filters::Duration::Short => "EgIYAfABAQ%3D%3D", + Invidious::Search::Filters::Duration::Medium => "EgIYA_ABAQ%3D%3D", + Invidious::Search::Filters::Duration::Long => "EgIYAvABAQ%3D%3D", } FEATURE_FILTERS = { - Invidious::Search::Filters::Features::Live => "EgJAAQ%3D%3D", - Invidious::Search::Filters::Features::FourK => "EgJwAQ%3D%3D", - Invidious::Search::Filters::Features::HD => "EgIgAQ%3D%3D", - Invidious::Search::Filters::Features::Subtitles => "EgIoAQ%3D%3D", - Invidious::Search::Filters::Features::CCommons => "EgIwAQ%3D%3D", - Invidious::Search::Filters::Features::ThreeSixty => "EgJ4AQ%3D%3D", - Invidious::Search::Filters::Features::VR180 => "EgPQAQE%3D", - Invidious::Search::Filters::Features::ThreeD => "EgI4AQ%3D%3D", - Invidious::Search::Filters::Features::HDR => "EgPIAQE%3D", - Invidious::Search::Filters::Features::Location => "EgO4AQE%3D", - Invidious::Search::Filters::Features::Purchased => "EgJIAQ%3D%3D", + Invidious::Search::Filters::Features::Live => "EgJAAfABAQ%3D%3D", + Invidious::Search::Filters::Features::FourK => "EgJwAfABAQ%3D%3D", + Invidious::Search::Filters::Features::HD => "EgIgAfABAQ%3D%3D", + Invidious::Search::Filters::Features::Subtitles => "EgIoAfABAQ%3D%3D", + Invidious::Search::Filters::Features::CCommons => "EgIwAfABAQ%3D%3D", + Invidious::Search::Filters::Features::ThreeSixty => "EgJ4AfABAQ%3D%3D", + Invidious::Search::Filters::Features::VR180 => "EgPQAQHwAQE%3D", + Invidious::Search::Filters::Features::ThreeD => "EgI4AfABAQ%3D%3D", + Invidious::Search::Filters::Features::HDR => "EgPIAQHwAQE%3D", + Invidious::Search::Filters::Features::Location => "EgO4AQHwAQE%3D", + Invidious::Search::Filters::Features::Purchased => "EgJIAfABAQ%3D%3D", } SORT_FILTERS = { - Invidious::Search::Filters::Sort::Relevance => "", - Invidious::Search::Filters::Sort::Date => "CAI%3D", - Invidious::Search::Filters::Sort::Views => "CAM%3D", - Invidious::Search::Filters::Sort::Rating => "CAE%3D", + Invidious::Search::Filters::Sort::Relevance => "8AEB", + Invidious::Search::Filters::Sort::Date => "CALwAQE%3D", + Invidious::Search::Filters::Sort::Views => "CAPwAQE%3D", + Invidious::Search::Filters::Sort::Rating => "CAHwAQE%3D", } Spectator.describe Invidious::Search::Filters do diff --git a/src/invidious/search/filters.cr b/src/invidious/search/filters.cr index c2b5c758..bf968734 100644 --- a/src/invidious/search/filters.cr +++ b/src/invidious/search/filters.cr @@ -300,9 +300,9 @@ module Invidious::Search object["9:varint"] = ((page - 1) * 20).to_i64 end - # If the object is empty, return an empty string, - # otherwise encode to protobuf then to base64 - return "" if object.empty? + # Prevent censoring of self harm topics + # See https://github.com/iv-org/invidious/issues/4398 + object["30:varint"] = 1.to_i64 return object .try { |i| Protodec::Any.cast_json(i) } From 0ad2eff2a46c28a877de1960a2dc5c15c0f94444 Mon Sep 17 00:00:00 2001 From: syeopite Date: Tue, 30 Jan 2024 15:25:45 -0800 Subject: [PATCH 501/598] WebVTT::Builder: Add logic to escape special chars --- spec/helpers/vtt/builder_spec.cr | 65 +++++++++++++++++++++----------- src/invidious/helpers/webvtt.cr | 16 +++++++- 2 files changed, 59 insertions(+), 22 deletions(-) diff --git a/spec/helpers/vtt/builder_spec.cr b/spec/helpers/vtt/builder_spec.cr index 7b543ddc..dc1f4613 100644 --- a/spec/helpers/vtt/builder_spec.cr +++ b/spec/helpers/vtt/builder_spec.cr @@ -1,34 +1,27 @@ require "../../spec_helper.cr" -MockLines = [ - { - "start_time": Time::Span.new(seconds: 1), - "end_time": Time::Span.new(seconds: 2), - "text": "Line 1", - }, - - { - "start_time": Time::Span.new(seconds: 2), - "end_time": Time::Span.new(seconds: 3), - "text": "Line 2", - }, -] +MockLines = ["Line 1", "Line 2"] +MockLinesWithEscapableCharacter = ["", "&Line 2>", '\u200E' + "Line\u200F 3", "\u00A0Line 4"] Spectator.describe "WebVTT::Builder" do it "correctly builds a vtt file" do result = WebVTT.build do |vtt| - MockLines.each do |line| - vtt.cue(line["start_time"], line["end_time"], line["text"]) + 2.times do |i| + vtt.cue( + Time::Span.new(seconds: i), + Time::Span.new(seconds: i + 1), + MockLines[i] + ) end end expect(result).to eq([ "WEBVTT", "", - "00:00:01.000 --> 00:00:02.000", + "00:00:00.000 --> 00:00:01.000", "Line 1", "", - "00:00:02.000 --> 00:00:03.000", + "00:00:01.000 --> 00:00:02.000", "Line 2", "", "", @@ -42,8 +35,12 @@ Spectator.describe "WebVTT::Builder" do } result = WebVTT.build(setting_fields) do |vtt| - MockLines.each do |line| - vtt.cue(line["start_time"], line["end_time"], line["text"]) + 2.times do |i| + vtt.cue( + Time::Span.new(seconds: i), + Time::Span.new(seconds: i + 1), + MockLines[i] + ) end end @@ -52,13 +49,39 @@ Spectator.describe "WebVTT::Builder" do "Kind: captions", "Language: en", "", - "00:00:01.000 --> 00:00:02.000", + "00:00:00.000 --> 00:00:01.000", "Line 1", "", - "00:00:02.000 --> 00:00:03.000", + "00:00:01.000 --> 00:00:02.000", "Line 2", "", "", ].join('\n')) end + + it "properly escapes characters" do + result = WebVTT.build do |vtt| + 4.times do |i| + vtt.cue(Time::Span.new(seconds: i), Time::Span.new(seconds: i + 1), MockLinesWithEscapableCharacter[i]) + end + end + + expect(result).to eq([ + "WEBVTT", + "", + "00:00:00.000 --> 00:00:01.000", + "<Line 1>", + "", + "00:00:01.000 --> 00:00:02.000", + "&Line 2>", + "", + "00:00:02.000 --> 00:00:03.000", + "‎Line‏ 3", + "", + "00:00:03.000 --> 00:00:04.000", + " Line 4", + "", + "", + ].join('\n')) + end end diff --git a/src/invidious/helpers/webvtt.cr b/src/invidious/helpers/webvtt.cr index 56f761ed..260d250f 100644 --- a/src/invidious/helpers/webvtt.cr +++ b/src/invidious/helpers/webvtt.cr @@ -4,13 +4,23 @@ module WebVTT # A WebVTT builder generates WebVTT files private class Builder + # See https://developer.mozilla.org/en-US/docs/Web/API/WebVTT_API#cue_payload + private ESCAPE_SUBSTITUTIONS = { + '&' => "&", + '<' => "<", + '>' => ">", + '\u200E' => "‎", + '\u200F' => "‏", + '\u00A0' => " ", + } + def initialize(@io : IO) end # Writes an vtt cue with the specified time stamp and contents def cue(start_time : Time::Span, end_time : Time::Span, text : String) timestamp(start_time, end_time) - @io << text + @io << self.escape(text) @io << "\n\n" end @@ -29,6 +39,10 @@ module WebVTT @io << '.' << timestamp.milliseconds.to_s.rjust(3, '0') end + private def escape(text : String) : String + return text.gsub(ESCAPE_SUBSTITUTIONS) + end + def document(setting_fields : Hash(String, String)? = nil, &) @io << "WEBVTT\n" From c864a63b6d86cb7552d2f1730731e427fc435fe4 Mon Sep 17 00:00:00 2001 From: shironeko Date: Thu, 8 Feb 2024 17:05:11 -0500 Subject: [PATCH 502/598] Fix pubsub feed parsing similar to what's done in #3793, this is causing an assert on my instance --- src/invidious/routes/feeds.cr | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/invidious/routes/feeds.cr b/src/invidious/routes/feeds.cr index 40bca008..512d4ee7 100644 --- a/src/invidious/routes/feeds.cr +++ b/src/invidious/routes/feeds.cr @@ -407,12 +407,17 @@ module Invidious::Routes::Feeds end spawn do - rss = XML.parse_html(body) - rss.xpath_nodes("//feed/entry").each do |entry| - id = entry.xpath_node("videoid").not_nil!.content - author = entry.xpath_node("author/name").not_nil!.content - published = Time.parse_rfc3339(entry.xpath_node("published").not_nil!.content) - updated = Time.parse_rfc3339(entry.xpath_node("updated").not_nil!.content) + # TODO: unify this with the other almost identical looking parts in this and channels.cr somehow? + namespaces = { + "yt" => "http://www.youtube.com/xml/schemas/2015", + "default" => "http://www.w3.org/2005/Atom", + } + rss = XML.parse(body) + rss.xpath_nodes("//default:feed/default:entry", namespaces).each do |entry| + id = entry.xpath_node("yt:videoId", namespaces).not_nil!.content + author = entry.xpath_node("default:author/default:name", namespaces).not_nil!.content + published = Time.parse_rfc3339(entry.xpath_node("default:published", namespaces).not_nil!.content) + updated = Time.parse_rfc3339(entry.xpath_node("default:updated", namespaces).not_nil!.content) video = get_video(id, force_refresh: true) From 98c421e9f539f8d72e6842fea94d17ff0db7f38a Mon Sep 17 00:00:00 2001 From: shironeko Date: Thu, 8 Feb 2024 18:58:23 -0500 Subject: [PATCH 503/598] Fix when video from pubsub is a scheduled event --- src/invidious/routes/feeds.cr | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/invidious/routes/feeds.cr b/src/invidious/routes/feeds.cr index 512d4ee7..e20a7139 100644 --- a/src/invidious/routes/feeds.cr +++ b/src/invidious/routes/feeds.cr @@ -419,7 +419,11 @@ module Invidious::Routes::Feeds published = Time.parse_rfc3339(entry.xpath_node("default:published", namespaces).not_nil!.content) updated = Time.parse_rfc3339(entry.xpath_node("default:updated", namespaces).not_nil!.content) - video = get_video(id, force_refresh: true) + begin + video = get_video(id, force_refresh: true) + rescue + next # skip this video since it raised an exception (e.g. it is a scheduled live event) + end if CONFIG.enable_user_notifications # Deliver notifications to `/api/v1/auth/notifications` From 6b33820f1f13171c3b432d6bc548b23380a3790d Mon Sep 17 00:00:00 2001 From: shironeko Date: Thu, 8 Feb 2024 18:23:08 -0500 Subject: [PATCH 504/598] Add missing translation strings closes #3120 --- locales/en-US.json | 5 +++++ src/invidious/frontend/comments_reddit.cr | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/locales/en-US.json b/locales/en-US.json index a9f78165..29fc7db6 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -1,4 +1,9 @@ { + "Add to playlist": "Add to playlist", + "Add to playlist: ": "Add to playlist: ", + "Answer": "Answer", + "Search for videos": "Search for videos", + "The Popular feed has been disabled by the administrator.": "The Popular feed has been disabled by the administrator.", "generic_channels_count": "{{count}} channel", "generic_channels_count_plural": "{{count}} channels", "generic_views_count": "{{count}} view", diff --git a/src/invidious/frontend/comments_reddit.cr b/src/invidious/frontend/comments_reddit.cr index b5647bae..4dda683e 100644 --- a/src/invidious/frontend/comments_reddit.cr +++ b/src/invidious/frontend/comments_reddit.cr @@ -33,7 +33,7 @@ module Invidious::Frontend::Comments [ − ] #{child.author} #{translate_count(locale, "comments_points_count", child.score, NumberFormatting::Separator)} - #{translate(locale, "`x` ago", recode_date(child.created_utc, locale))} + #{translate(locale, "`x` ago", recode_date(child.created_utc, locale))} #{translate(locale, "permalink")}

    From 72bcd3cc72cf10bda461235a39d18eee15130014 Mon Sep 17 00:00:00 2001 From: absidue <48293849+absidue@users.noreply.github.com> Date: Mon, 12 Feb 2024 18:55:15 +0100 Subject: [PATCH 505/598] Handle non-200 status codes for YouTube DASH manifests --- src/invidious/routes/api/manifest.cr | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/invidious/routes/api/manifest.cr b/src/invidious/routes/api/manifest.cr index 662d1002..d89e752c 100644 --- a/src/invidious/routes/api/manifest.cr +++ b/src/invidious/routes/api/manifest.cr @@ -21,7 +21,13 @@ module Invidious::Routes::API::Manifest end if dashmpd = video.dash_manifest_url - manifest = YT_POOL.client &.get(URI.parse(dashmpd).request_target).body + response = YT_POOL.client &.get(URI.parse(dashmpd).request_target) + + if response.status_code != 200 + haltf env, status_code: response.status_code + end + + manifest = response.body manifest = manifest.gsub(/[^<]+<\/BaseURL>/) do |baseurl| url = baseurl.lchop("") From 7b84bdb29b60504c1c5c88617e191767803384ab Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Tue, 13 Feb 2024 21:05:26 +0100 Subject: [PATCH 506/598] API: Add APIHandler back This handler should no have been removed in 4276, as it adds the required CORS header (Access-Control-Allow-Origin) for public acces to the API. Thanks to iBicha for noticing this! --- src/invidious.cr | 1 + src/invidious/helpers/handlers.cr | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/invidious.cr b/src/invidious.cr index c8cac80e..e0bd0101 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -217,6 +217,7 @@ public_folder "assets" Kemal.config.powered_by_header = false add_handler FilteredCompressHandler.new +add_handler APIHandler.new add_handler AuthHandler.new add_handler DenyFrame.new add_context_storage_type(Array(String)) diff --git a/src/invidious/helpers/handlers.cr b/src/invidious/helpers/handlers.cr index cece289b..174f620d 100644 --- a/src/invidious/helpers/handlers.cr +++ b/src/invidious/helpers/handlers.cr @@ -134,6 +134,19 @@ class AuthHandler < Kemal::Handler end end +class APIHandler < Kemal::Handler + {% for method in %w(GET POST PUT HEAD DELETE PATCH OPTIONS) %} + only ["/api/v1/*"], {{method}} + {% end %} + exclude ["/api/v1/auth/notifications"], "GET" + exclude ["/api/v1/auth/notifications"], "POST" + + def call(env) + env.response.headers["Access-Control-Allow-Origin"] = "*" if only_match?(env) + call_next env + end +end + class DenyFrame < Kemal::Handler exclude ["/embed/*"] From c52c6d3c9a90015dab3f2c3aa0d6291319d07409 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 15 Feb 2024 18:02:14 +0100 Subject: [PATCH 507/598] Update Turkish translation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update Turkish translation Co-authored-by: Hosted Weblate Co-authored-by: Oğuz Ersen --- locales/tr.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/locales/tr.json b/locales/tr.json index 0575a4dd..d25cfd65 100644 --- a/locales/tr.json +++ b/locales/tr.json @@ -486,5 +486,7 @@ "playlist_button_add_items": "Video ekle", "channel_tab_podcasts_label": "Podcast'ler", "generic_channels_count": "{{count}} kanal", - "generic_channels_count_plural": "{{count}} kanal" + "generic_channels_count_plural": "{{count}} kanal", + "Import YouTube watch history (.json)": "YouTube İzleme Geçmişini İçe Aktar (.json)", + "toggle_theme": "Temayı Değiştir" } From 736f35332a2fe437d163f39e5261379ca7797e48 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 15 Feb 2024 18:02:14 +0100 Subject: [PATCH 508/598] Update Portuguese (Brazil) translation Update Portuguese (Brazil) translation Co-authored-by: Hosted Weblate Co-authored-by: joaooliva --- locales/pt-BR.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/locales/pt-BR.json b/locales/pt-BR.json index 1e089723..af14eb29 100644 --- a/locales/pt-BR.json +++ b/locales/pt-BR.json @@ -503,5 +503,7 @@ "generic_button_rss": "RSS", "generic_channels_count_0": "{{count}} canal", "generic_channels_count_1": "{{count}} canais", - "generic_channels_count_2": "{{count}} canais" + "generic_channels_count_2": "{{count}} canais", + "Import YouTube watch history (.json)": "Importar histórico de reprodução do YouTube (.json)", + "toggle_theme": "Alternar Tema" } From 8ffc569ebd7752183a39e412c6e9e8451e7b852c Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 15 Feb 2024 18:02:14 +0100 Subject: [PATCH 509/598] Update German translation Update German translation Co-authored-by: Hosted Weblate Co-authored-by: Lenny Angst Co-authored-by: Radoslav Lelchev --- locales/de.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/locales/de.json b/locales/de.json index 59c6a49c..756aff76 100644 --- a/locales/de.json +++ b/locales/de.json @@ -148,7 +148,7 @@ "Whitelisted regions: ": "Erlaubte Regionen: ", "Blacklisted regions: ": "Unerlaubte Regionen: ", "Shared `x`": "Geteilt `x`", - "Premieres in `x`": "Zuerst gesehen in `x`", + "Premieres in `x`": "Premiere in `x`", "Premieres `x`": "Erster Start `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.": "Hallo! Anscheinend haben Sie JavaScript deaktiviert. Klicken Sie hier um Kommentare anzuzeigen, beachten sie dass es etwas länger dauern kann um sie zu laden.", "View YouTube comments": "YouTube Kommentare anzeigen", @@ -486,5 +486,6 @@ "channel_tab_podcasts_label": "Podcasts", "channel_tab_releases_label": "Veröffentlichungen", "generic_channels_count": "{{count}} Kanal", - "generic_channels_count_plural": "{{count}} Kanäle" + "generic_channels_count_plural": "{{count}} Kanäle", + "Import YouTube watch history (.json)": "YouTube Wiedergabeverlauf importieren (.json)" } From 8169cd8977c8ce93cdead95c00dbdd748c4f7f31 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 15 Feb 2024 18:02:14 +0100 Subject: [PATCH 510/598] Update Danish translation Co-authored-by: Grooty12 Co-authored-by: Hosted Weblate --- locales/da.json | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/locales/da.json b/locales/da.json index 16607546..019f1c51 100644 --- a/locales/da.json +++ b/locales/da.json @@ -452,5 +452,40 @@ "crash_page_you_found_a_bug": "Det ser ud til, at du har fundet en fejl i Invidious!", "crash_page_read_the_faq": "læs Ofte stillede spørgsmål (FAQ)", "crash_page_search_issue": "søgte efter eksisterende problemer på GitHub", - "search_filters_title": "Filter" + "search_filters_title": "Filter", + "playlist_button_add_items": "Tilføj videoer", + "search_message_no_results": "Ingen resultater fundet.", + "Import YouTube watch history (.json)": "Importer YouTube afspilningshistorik (.json)", + "search_message_change_filters_or_query": "Prøv at udvide din søgeforspørgsel og/eller ændre filtrene.", + "search_message_use_another_instance": " Du kan også søge på en anden instans.", + "Music in this video": "Musik i denne video", + "search_filters_date_option_none": "Enhver dato", + "search_filters_type_option_all": "Enhver type", + "search_filters_duration_option_none": "Enhver varighed", + "search_filters_duration_option_medium": "Medium (4 - 20 minutter)", + "search_filters_features_option_vr180": "VR180", + "generic_channels_count": "{{count}} kanal", + "generic_channels_count_plural": "{{count}} kanaler", + "Import YouTube playlist (.csv)": "Importer YouTube playliste (.csv)", + "Standard YouTube license": "Standard Youtube-licens", + "Album: ": "Album: ", + "Channel Sponsor": "Kanal-sponsor", + "Song: ": "Sang: ", + "channel_tab_playlists_label": "Playlister", + "channel_tab_channels_label": "Kanaler", + "Artist: ": "Kunstner: ", + "search_filters_date_label": "Uploaddato", + "generic_button_delete": "Slet", + "generic_button_edit": "Rediger", + "generic_button_save": "Gem", + "generic_button_cancel": "Afbryd", + "generic_button_rss": "RSS", + "Popular enabled: ": "Populær aktiveret: ", + "search_filters_apply_button": "Anvend udvalgte filtre", + "channel_tab_shorts_label": "Shorts", + "channel_tab_streams_label": "Livestreams", + "channel_tab_podcasts_label": "Podcasts", + "channel_tab_releases_label": "Udgivelser", + "Download is disabled": "Download er slået fra", + "error_video_not_in_playlist": "Den ønskede video findes ikke i denne playliste. Klik her for playlistens startside." } From 8cec7ba0040c8fb058a29b23d011235b9793a55a Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 15 Feb 2024 18:02:14 +0100 Subject: [PATCH 511/598] Update Russian translation Update Russian translation Co-authored-by: Hosted Weblate Co-authored-by: Noise Maker Co-authored-by: hikiko4ern <25303622+hikiko4ern@users.noreply.github.com> --- locales/ru.json | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/locales/ru.json b/locales/ru.json index 2769f3ab..61bf9e92 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -8,14 +8,14 @@ "newest": "сначала новые", "oldest": "сначала старые", "popular": "популярные", - "last": "недавние", + "last": "последние", "Next page": "Следующая страница", "Previous page": "Предыдущая страница", "Clear watch history?": "Очистить историю просмотров?", "New password": "Новый пароль", "New passwords must match": "Новые пароли не совпадают", "Authorize token?": "Авторизовать токен?", - "Authorize token for `x`?": "Авторизовать токен для `x`?", + "Authorize token for `x`?": "Токен авторизации для `x`?", "Yes": "Да", "No": "Нет", "Import and Export Data": "Импорт и экспорт данных", @@ -29,7 +29,7 @@ "Export subscriptions as OPML": "Экспортировать подписки в формате OPML", "Export subscriptions as OPML (for NewPipe & FreeTube)": "Экспортировать подписки в формате OPML (для NewPipe и FreeTube)", "Export data as JSON": "Экспортировать данные Invidious в формате JSON", - "Delete account?": "Удалить учётку?", + "Delete account?": "Удалить учётную запись?", "History": "История", "An alternative front-end to YouTube": "Альтернативный фронтенд для YouTube", "JavaScript license information": "Информация о лицензиях JavaScript", @@ -42,7 +42,7 @@ "Text CAPTCHA": "Текстовая капча (англ.)", "Image CAPTCHA": "Капча-картинка", "Sign In": "Войти", - "Register": "Зарегистрироваться", + "Register": "Регистрация", "E-mail": "Эл. почта", "Preferences": "Настройки", "preferences_category_player": "Настройки проигрывателя", @@ -61,7 +61,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": "Настройки сайта", @@ -77,13 +77,13 @@ "preferences_annotations_subscribed_label": "Всегда показывать аннотации на каналах из ваших подписок? ", "Redirect homepage to feed: ": "Показывать подписки на главной странице: ", "preferences_max_results_label": "Число видео в ленте: ", - "preferences_sort_label": "Сортировать видео: ", - "published": "по дате публикации", - "published - reverse": "по дате публикации в обратном порядке", - "alphabetically": "по алфавиту", - "alphabetically - reverse": "по алфавиту в обратном порядке", - "channel name": "по названию канала", - "channel name - reverse": "по названию канала в обратном порядке", + "preferences_sort_label": "Сортировать видео по: ", + "published": "дате публикации", + "published - reverse": "дате публикации в обратном порядке", + "alphabetically": "алфавиту", + "alphabetically - reverse": "алфавиту в обратном порядке", + "channel name": "названию канала", + "channel name - reverse": "названию канала в обратном порядке", "Only show latest video from channel: ": "Показывать только последние видео с каналов: ", "Only show latest unwatched video from channel: ": "Показывать только последние непросмотренные видео с канала: ", "preferences_unseen_only_label": "Показывать только непросмотренные видео: ", @@ -134,8 +134,8 @@ "Title": "Заголовок", "Playlist privacy": "Видимость плейлиста", "Editing playlist `x`": "Редактирование плейлиста `x`", - "Show more": "Развернуть", - "Show less": "Свернуть", + "Show more": "Показать больше", + "Show less": "Показать меньше", "Watch on YouTube": "Смотреть на YouTube", "Switch Invidious Instance": "Сменить зеркало Invidious", "Hide annotations": "Скрыть аннотации", @@ -414,7 +414,7 @@ "generic_count_days_0": "{{count}} день", "generic_count_days_1": "{{count}} дня", "generic_count_days_2": "{{count}} дней", - "preferences_quality_dash_option_auto": "Автоматическое", + "preferences_quality_dash_option_auto": "Авто", "preferences_quality_dash_option_1080p": "1080p", "preferences_quality_dash_option_720p": "720p", "generic_subscriptions_count_0": "{{count}} подписка", @@ -466,7 +466,7 @@ "search_filters_features_option_three_sixty": "360°", "Video unavailable": "Видео недоступно", "preferences_save_player_pos_label": "Запоминать позицию: ", - "preferences_region_label": "Страна: ", + "preferences_region_label": "Страна источник ", "preferences_watch_history_label": "Включить историю просмотров: ", "search_filters_title": "Фильтр", "search_filters_duration_option_none": "Любой длины", @@ -476,7 +476,7 @@ "search_message_no_results": "Ничего не найдено.", "search_message_use_another_instance": " Дополнительно вы можете поискать на других зеркалах.", "search_filters_features_option_vr180": "VR180", - "search_message_change_filters_or_query": "Попробуйте расширить поисковый запрос или изменить фильтры.", + "search_message_change_filters_or_query": "Попробуйте расширить поисковый запрос и/или изменить фильтры.", "search_filters_duration_option_medium": "Средние (4 - 20 минут)", "search_filters_apply_button": "Применить фильтры", "Popular enabled: ": "Популярное включено: ", @@ -503,5 +503,6 @@ "channel_tab_podcasts_label": "Подкасты", "generic_channels_count_0": "{{count}} канал", "generic_channels_count_1": "{{count}} канала", - "generic_channels_count_2": "{{count}} каналов" + "generic_channels_count_2": "{{count}} каналов", + "Import YouTube watch history (.json)": "Импортировать историю просмотра из YouTube (.json)" } From f21a532c0d2fcc202317f41401596866543108a4 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 15 Feb 2024 18:02:14 +0100 Subject: [PATCH 512/598] Update Bulgarian translation Co-authored-by: Hosted Weblate Co-authored-by: Radoslav Lelchev --- locales/bg.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/bg.json b/locales/bg.json index 82591ed8..bcce6a7a 100644 --- a/locales/bg.json +++ b/locales/bg.json @@ -486,5 +486,6 @@ "preferences_annotations_label": "Покажи анотаций по подразбиране: ", "generic_views_count": "{{count}} гледане", "generic_views_count_plural": "{{count}} гледания", - "Next page": "Следваща страница" + "Next page": "Следваща страница", + "Import YouTube watch history (.json)": "Импортиране на историята на гледане от YouTube (.json)" } From f062c18b8247caba486bc7013f57d17fca195389 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 15 Feb 2024 18:02:14 +0100 Subject: [PATCH 513/598] Update Ukrainian translation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update Ukrainian translation Update Ukrainian translation Co-authored-by: Hosted Weblate Co-authored-by: Ihor Hordiichuk Co-authored-by: Сергій --- locales/uk.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/locales/uk.json b/locales/uk.json index c26618fe..f9640bba 100644 --- a/locales/uk.json +++ b/locales/uk.json @@ -503,5 +503,7 @@ "generic_button_save": "Зберегти", "generic_channels_count_0": "{{count}} канал", "generic_channels_count_1": "{{count}} канали", - "generic_channels_count_2": "{{count}} каналів" + "generic_channels_count_2": "{{count}} каналів", + "Import YouTube watch history (.json)": "Імпортувати історію переглядів YouTube (.json)", + "toggle_theme": "Перемкнути тему" } From b9ae1a61da4dc9cdd3191b8e16d542e053e1b727 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 15 Feb 2024 18:02:14 +0100 Subject: [PATCH 514/598] Update Japanese translation Update Japanese translation Update Japanese translation Update Japanese translation Update Japanese translation Update Japanese translation Co-authored-by: Hosted Weblate Co-authored-by: maboroshin --- locales/ja.json | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/locales/ja.json b/locales/ja.json index 17e60998..2e3437bc 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -53,7 +53,7 @@ "preferences_category_player": "プレイヤーの設定", "preferences_video_loop_label": "常にループ: ", "preferences_autoplay_label": "自動再生: ", - "preferences_continue_label": "次の動画を自動再生: ", + "preferences_continue_label": "次の動画に移動: ", "preferences_continue_autoplay_label": "次の動画を自動再生: ", "preferences_listen_label": "音声モードを使用: ", "preferences_local_label": "動画視聴にプロキシを経由: ", @@ -68,7 +68,7 @@ "preferences_related_videos_label": "関連動画を表示: ", "preferences_annotations_label": "最初からアノテーションを表示: ", "preferences_extend_desc_label": "動画の説明文を自動的に拡張: ", - "preferences_vr_mode_label": "対話的な360°動画 (WebGL が必要): ", + "preferences_vr_mode_label": "対話的な360°動画 (WebGLが必要): ", "preferences_category_visual": "外観設定", "preferences_player_style_label": "プレイヤーのスタイル: ", "Dark mode: ": "ダークモード: ", @@ -125,9 +125,9 @@ "subscriptions_unseen_notifs_count_0": "{{count}}件の未読通知", "search": "検索", "Log out": "ログアウト", - "Released under the AGPLv3 on Github.": "GitHub 上で AGPLv3 の元で公開", + "Released under the AGPLv3 on Github.": "GitHub上でAGPLv3の元で公開", "Source available here.": "ソースはここで閲覧可能です。", - "View JavaScript license information.": "JavaScript ライセンス情報", + "View JavaScript license information.": "JavaScriptライセンス情報", "View privacy policy.": "個人情報保護方針", "Trending": "急上昇", "Public": "公開", @@ -144,7 +144,7 @@ "Show more": "もっと見る", "Show less": "表示を少なく", "Watch on YouTube": "YouTubeで視聴", - "Switch Invidious Instance": "Invidious インスタンスの変更", + "Switch Invidious Instance": "Invidiousインスタンスの変更", "Hide annotations": "アノテーションを隠す", "Show annotations": "アノテーションを表示", "Genre: ": "ジャンル: ", @@ -363,9 +363,9 @@ "search_filters_features_option_location": "場所", "search_filters_features_option_hdr": "HDR", "Current version: ": "現在のバージョン: ", - "next_steps_error_message": "下記のものを試して下さい: ", - "next_steps_error_message_refresh": "再読込", - "next_steps_error_message_go_to_youtube": "YouTubeへ", + "next_steps_error_message": "以下をお試してください: ", + "next_steps_error_message_refresh": "再読み込み", + "next_steps_error_message_go_to_youtube": "YouTubeを開く", "search_filters_duration_option_short": "4分未満", "footer_documentation": "説明書", "footer_source_code": "ソースコード", @@ -459,7 +459,7 @@ "Song: ": "曲: ", "Channel Sponsor": "チャンネルのスポンサー", "Standard YouTube license": "標準 Youtube ライセンス", - "Download is disabled": "ダウンロード: このインスタンスでは未対応", + "Download is disabled": "ダウンロード: このインスタンスは未対応", "Import YouTube playlist (.csv)": "YouTube 再生リストをインポート (.csv)", "generic_button_delete": "削除", "generic_button_cancel": "キャンセル", @@ -469,5 +469,6 @@ "generic_button_save": "保存", "generic_button_rss": "RSS", "playlist_button_add_items": "動画を追加", - "generic_channels_count_0": "{{count}}個のチャンネル" + "generic_channels_count_0": "{{count}}個のチャンネル", + "Import YouTube watch history (.json)": "YouTube 視聴履歴をインポート (.json)" } From 7e1deea15e4e27d965a0ece45ca4e0e678df01c9 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 15 Feb 2024 18:02:14 +0100 Subject: [PATCH 515/598] Update Catalan translation Co-authored-by: Hosted Weblate Co-authored-by: victor dargallo --- locales/ca.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/ca.json b/locales/ca.json index a718eb2b..4ae55804 100644 --- a/locales/ca.json +++ b/locales/ca.json @@ -486,5 +486,6 @@ "generic_channels_count_plural": "{{count}} canals", "generic_button_edit": "Edita", "generic_button_rss": "RSS", - "generic_button_delete": "Suprimeix" + "generic_button_delete": "Suprimeix", + "Import YouTube watch history (.json)": "Importa l'historial de visualitzacions de YouTube (.json)" } From 833c711cba15ec28709b97b13b54abfe11a17ff8 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 15 Feb 2024 18:02:14 +0100 Subject: [PATCH 516/598] Update Czech translation Update Czech translation Co-authored-by: Fjuro Co-authored-by: Hosted Weblate --- locales/cs.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/locales/cs.json b/locales/cs.json index 10c114eb..4aa20f28 100644 --- a/locales/cs.json +++ b/locales/cs.json @@ -503,5 +503,7 @@ "playlist_button_add_items": "Přidat videa", "generic_channels_count_0": "{{count}} kanál", "generic_channels_count_1": "{{count}} kanály", - "generic_channels_count_2": "{{count}} kanálů" + "generic_channels_count_2": "{{count}} kanálů", + "Import YouTube watch history (.json)": "Importovat historii sledování z YouTube (.json)", + "toggle_theme": "Přepnout motiv" } From 4aed0e1102750b0ab0a740a81df5d0523a41fca1 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 15 Feb 2024 18:02:14 +0100 Subject: [PATCH 517/598] Update Portuguese translation Update Portuguese translation Update Portuguese translation Update Portuguese translation Co-authored-by: Filipe Martins Co-authored-by: Hosted Weblate Co-authored-by: Jener Gomes Co-authored-by: SC Co-authored-by: jamerLamer --- locales/pt.json | 99 ++++++++++++++++++++++++++++++------------------- 1 file changed, 60 insertions(+), 39 deletions(-) diff --git a/locales/pt.json b/locales/pt.json index e7cc4810..c1d8b5b4 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -1,7 +1,7 @@ { - "search_filters_type_option_show": "Espetáculo", + "search_filters_type_option_show": "Série", "search_filters_sort_option_views": "Visualizações", - "search_filters_sort_option_date": "Data de envio", + "search_filters_sort_option_date": "Data de carregamento", "search_filters_sort_option_rating": "Avaliação", "search_filters_sort_option_relevance": "Relevância", "Switch Invidious Instance": "Mudar a instância do Invidious", @@ -13,7 +13,7 @@ "preferences_category_misc": "Preferências diversas", "preferences_vr_mode_label": "Vídeos interativos de 360 graus (necessita de WebGL): ", "preferences_extend_desc_label": "Estender automaticamente a descrição do vídeo: ", - "next_steps_error_message_go_to_youtube": "Ir ao YouTube", + "next_steps_error_message_go_to_youtube": "Ir para o YouTube", "next_steps_error_message": "Pode tentar as seguintes opções: ", "next_steps_error_message_refresh": "Atualizar", "search_filters_features_option_hdr": "HDR", @@ -44,20 +44,27 @@ "Default": "Predefinido", "Top": "Destaques", "Search": "Pesquisar", - "generic_count_years": "{{count}} segundo", - "generic_count_years_plural": "{{count}} segundos", - "generic_count_months": "{{count}} minuto", - "generic_count_months_plural": "{{count}} minutos", - "generic_count_weeks": "{{count}} hora", - "generic_count_weeks_plural": "{{count}} horas", - "generic_count_days": "{{count}} dia", - "generic_count_days_plural": "{{count}} dias", - "generic_count_hours": "{{count}} seman", - "generic_count_hours_plural": "{{count}} semanas", - "generic_count_minutes": "{{count}} mês", - "generic_count_minutes_plural": "{{count}} meses", - "generic_count_seconds": "{{count}} ano", - "generic_count_seconds_plural": "{{count}} anos", + "generic_count_years_0": "{{count}} ano", + "generic_count_years_1": "{{count}} anos", + "generic_count_years_2": "{{count}} anos", + "generic_count_months_0": "{{count}} mês", + "generic_count_months_1": "{{count}} meses", + "generic_count_months_2": "{{count}} meses", + "generic_count_weeks_0": "{{count}} semana", + "generic_count_weeks_1": "{{count}} semanas", + "generic_count_weeks_2": "{{count}} semanas", + "generic_count_days_0": "{{count}} dia", + "generic_count_days_1": "{{count}} dias", + "generic_count_days_2": "{{count}} dias", + "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", "Chinese (Traditional)": "Chinês (tradicional)", "Chinese (Simplified)": "Chinês (simplificado)", "Could not pull trending pages.": "Não foi possível obter as páginas de tendências.", @@ -75,7 +82,7 @@ "Import/export data": "Importar / exportar dados", "preferences_annotations_label": "Mostrar anotações sempre: ", "preferences_continue_label": "Reproduzir sempre o próximo: ", - "Sign In": "Iniciar sessão", + "Sign In": "Entrar", "Log in/register": "Iniciar sessão/registar", "Delete account?": "Eliminar conta?", "Import and Export Data": "Importar e exportar dados", @@ -167,8 +174,9 @@ "Log out": "Terminar sessão", "Subscriptions": "Subscrições", "revoke": "revogar", - "tokens_count": "{{count}} token", - "tokens_count_plural": "{{count}} tokens", + "tokens_count_0": "{{count}} Token", + "tokens_count_1": "{{count}} Tokens", + "tokens_count_2": "{{count}} Tokens", "Token": "Token", "Token manager": "Gerir tokens", "Subscription manager": "Gerir subscrições", @@ -402,31 +410,39 @@ "videoinfo_youTube_embed_link": "Incorporar", "preferences_save_player_pos_label": "Guardar a posição de reprodução atual do vídeo: ", "download_subtitles": "Legendas - `x` (.vtt)", - "generic_views_count": "{{count}} visualização", - "generic_views_count_plural": "{{count}} visualizações", + "generic_views_count_0": "{{count}} visualização", + "generic_views_count_1": "{{count}} visualizações", + "generic_views_count_2": "{{count}} visualizações", "videoinfo_started_streaming_x_ago": "Iniciou a transmissão há `x`", "user_saved_playlists": "`x` listas de reprodução guardadas", - "generic_videos_count": "{{count}} vídeo", - "generic_videos_count_plural": "{{count}} vídeos", - "generic_playlists_count": "{{count}} lista de reprodução", - "generic_playlists_count_plural": "{{count}} listas de reprodução", - "subscriptions_unseen_notifs_count": "{{count}} notificação não vista", - "subscriptions_unseen_notifs_count_plural": "{{count}} notificações não vistas", - "comments_view_x_replies": "Ver {{count}} resposta", - "comments_view_x_replies_plural": "Ver {{count}} respostas", - "generic_subscribers_count": "{{count}} inscrito", - "generic_subscribers_count_plural": "{{count}} inscritos", - "generic_subscriptions_count": "{{count}} inscrição", - "generic_subscriptions_count_plural": "{{count}} inscrições", - "comments_points_count": "{{count}} ponto", - "comments_points_count_plural": "{{count}} pontos", + "generic_videos_count_0": "{{count}} vídeo", + "generic_videos_count_1": "{{count}} vídeos", + "generic_videos_count_2": "{{count}} vídeos", + "generic_playlists_count_0": "{{count}} lista de reprodução", + "generic_playlists_count_1": "{{count}} listas de reprodução", + "generic_playlists_count_2": "{{count}} listas de reprodução", + "subscriptions_unseen_notifs_count_0": "{{count}} notificação não vista", + "subscriptions_unseen_notifs_count_1": "{{count}} notificações não vistas", + "subscriptions_unseen_notifs_count_2": "{{count}} notificações não vistas", + "comments_view_x_replies_0": "Ver {{count}} resposta", + "comments_view_x_replies_1": "Ver {{count}} respostas", + "comments_view_x_replies_2": "Ver {{count}} respostas", + "generic_subscribers_count_0": "{{count}} inscrito", + "generic_subscribers_count_1": "{{count}} inscritos", + "generic_subscribers_count_2": "{{count}} inscritos", + "generic_subscriptions_count_0": "{{count}} inscrição", + "generic_subscriptions_count_1": "{{count}} inscrições", + "generic_subscriptions_count_2": "{{count}} inscrições", + "comments_points_count_0": "{{count}} ponto", + "comments_points_count_1": "{{count}} pontos", + "comments_points_count_2": "{{count}} pontos", "crash_page_you_found_a_bug": "Parece que encontrou um erro no Invidious!", "crash_page_before_reporting": "Antes de reportar um erro, verifique se:", "crash_page_refresh": "tentou recarregar a página", "crash_page_switch_instance": "tentou usar outra instância", "crash_page_read_the_faq": "leia as Perguntas frequentes (FAQ)", "crash_page_search_issue": "procurou se o erro já foi reportado no GitHub", - "crash_page_report_issue": "Se nenhuma opção acima ajudou, por favor abra um novo problema no Github (preferencialmente em inglês) e inclua o seguinte texto tal qual (NÃO o traduza):", + "crash_page_report_issue": "Se nenhuma opção acima ajudou, por favor abra um novo problema no Github (preferencialmente em inglês) e inclua o seguinte texto (NÃO o traduza):", "user_created_playlists": "`x` listas de reprodução criadas", "search_filters_title": "Filtro", "Chinese (Taiwan)": "Chinês (Taiwan)", @@ -464,7 +480,7 @@ "search_filters_type_option_all": "Qualquer tipo", "search_filters_duration_option_none": "Qualquer duração", "Popular enabled: ": "Página \"popular\" ativada: ", - "error_video_not_in_playlist": "O vídeo pedido não existe nesta lista de reprodução. Clique aqui para a página inicial da lista de reprodução.", + "error_video_not_in_playlist": "O vídeo pedido não existe nesta lista de reprodução. Clique aqui para voltar à página inicial da lista de reprodução.", "channel_tab_playlists_label": "Listas de reprodução", "channel_tab_channels_label": "Canais", "channel_tab_shorts_label": "Curtos", @@ -484,5 +500,10 @@ "channel_tab_releases_label": "Lançamentos", "generic_button_save": "Salvar", "generic_button_cancel": "Cancelar", - "playlist_button_add_items": "Adicionar vídeos" + "playlist_button_add_items": "Adicionar vídeos", + "generic_channels_count_0": "{{count}} canal", + "generic_channels_count_1": "{{count}} canais", + "generic_channels_count_2": "{{count}} canais", + "Import YouTube watch history (.json)": "Importar histórico de reprodução do YouTube (.json)", + "toggle_theme": "Trocar tema" } From 99a3bd4fff5088c022156487ab4944dfdf9a1122 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 15 Feb 2024 18:02:14 +0100 Subject: [PATCH 518/598] Update Vietnamese translation Co-authored-by: Hosted Weblate Co-authored-by: Tran Viet Duc --- locales/vi.json | 309 ++++++++++++++++++++++++++++++------------------ 1 file changed, 196 insertions(+), 113 deletions(-) diff --git a/locales/vi.json b/locales/vi.json index 9cb87d3e..4f8dc30d 100644 --- a/locales/vi.json +++ b/locales/vi.json @@ -1,62 +1,62 @@ { "generic_videos_count_0": "{{count}} video", - "generic_subscribers_count_0": "{{count}} người theo dõi", + "generic_subscribers_count_0": "{{count}} người đăng ký", "LIVE": "TRỰC TIẾP", "Shared `x` ago": "Đã chia sẻ `x` trước", - "Unsubscribe": "Hủy theo dõi", - "Subscribe": "Theo dõi", + "Unsubscribe": "Hủy đăng ký", + "Subscribe": "Đăng ký", "View channel on YouTube": "Xem kênh trên YouTube", "View playlist on YouTube": "Xem danh sách phát trên YouTube", - "newest": "mới nhất", - "oldest": "lâu đời nhất", - "popular": "phổ biến", - "last": "Cuối cùng", + "newest": "Mới nhất", + "oldest": "Cũ nhất", + "popular": "Phổ biến", + "last": "cuối cùng", "Next page": "Trang tiếp theo", "Previous page": "Trang trước", "Clear watch history?": "Xóa lịch sử xem?", "New password": "Mật khẩu mới", "New passwords must match": "Mật khẩu mới phải khớp", "Authorize token?": "Cấp phép mã thông báo?", - "Authorize token for `x`?": "Cấp phép mã thông báo cho` x`?", - "Yes": "Đúng", + "Authorize token for `x`?": "Cấp phép mã thông báo cho `x`?", + "Yes": "Có", "No": "Không", "Import and Export Data": "Nhập và xuất dữ liệu", "Import": "Nhập", - "Import Invidious data": "Nhập dữ liệu Invidious JSON", - "Import YouTube subscriptions": "Nhập dữ liệu thuê bao YouTube/OPML", - "Import FreeTube subscriptions (.db)": "Nhập đăng ký FreeTube (.db)", - "Import NewPipe subscriptions (.json)": "Nhập đăng ký NewPipe (.json)", - "Import NewPipe data (.zip)": "Nhập dữ liệu NewPipe (.zip)", + "Import Invidious data": "Nhập dữ liệu Invidious dưới dạng JSON", + "Import YouTube subscriptions": "Nhập các kênh đã đăng ký từ YouTube/OPML", + "Import FreeTube subscriptions (.db)": "Nhập các kênh đã đăng ký từ FreeTube (.db)", + "Import NewPipe subscriptions (.json)": "Nhập các kênh đã đăng ký từ NewPipe (.json)", + "Import NewPipe data (.zip)": "Nhập dữ liệu từ NewPipe (.zip)", "Export": "Xuất", - "Export subscriptions as OPML": "Xuất đăng ký dưới dạng OPML", - "Export subscriptions as OPML (for NewPipe & FreeTube)": "Xuất đăng ký dưới dạng OPML (cho NewPipe & FreeTube)", + "Export subscriptions as OPML": "Xuất các kênh đã đăng ký dưới dạng OPML", + "Export subscriptions as OPML (for NewPipe & FreeTube)": "Xuất các kênh đã đăng ký dưới dạng OPML (cho NewPipe & FreeTube)", "Export data as JSON": "Xuất dữ liệu Invidious dưới dạng JSON", "Delete account?": "Xóa tài khoản?", "History": "Lịch sử", - "An alternative front-end to YouTube": "Giao diện người dùng thay thế cho YouTube", + "An alternative front-end to YouTube": "Một front-end thay thế cho YouTube", "JavaScript license information": "Thông tin giấy phép JavaScript", "source": "nguồn", "Log in": "Đăng nhập", "Log in/register": "Đăng nhập / đăng ký", - "User ID": "Tên người dùng", + "User ID": "ID người dùng", "Password": "Mật khẩu", - "Time (h:mm:ss):": "Thời gian (h: mm: ss):", - "Text CAPTCHA": "Nhắn tin tới CAPTCHA", - "Image CAPTCHA": "Hình ảnh CAPTCHA", + "Time (h:mm:ss):": "Thời gian (h:mm:ss):", + "Text CAPTCHA": "CAPTCHA dạng chữ", + "Image CAPTCHA": "CAPTCHA dạng ảnh", "Sign In": "Đăng nhập", "Register": "Đăng ký", "E-mail": "E-mail", "Preferences": "Sở thích", "preferences_category_player": "Tùy chọn trình phát video", "preferences_video_loop_label": "Luôn lặp lại: ", - "preferences_autoplay_label": "Tự chạy: ", + "preferences_autoplay_label": "Tự động phát: ", "preferences_continue_label": "Phát kế tiếp theo mặc định: ", "preferences_continue_autoplay_label": "Tự động phát video tiếp theo: ", "preferences_listen_label": "Nghe theo mặc định: ", "preferences_local_label": "Video proxy: ", "preferences_speed_label": "Tốc độ mặc định: ", "preferences_quality_label": "Chất lượng video ưa thích: ", - "preferences_volume_label": "Âm lượng trình phát video: ", + "preferences_volume_label": "Âm lượng video: ", "preferences_comments_label": "Nhận xét mặc định: ", "youtube": "YouTube", "reddit": "Reddit", @@ -64,7 +64,7 @@ "Fallback captions: ": "Phụ đề dự phòng: ", "preferences_related_videos_label": "Hiển thị các video có liên quan: ", "preferences_annotations_label": "Hiển thị chú thích theo mặc định: ", - "preferences_extend_desc_label": "Tự động mở rộng mô tả video: ", + "preferences_extend_desc_label": "Tự động mở rộng phần mô tả của video: ", "preferences_vr_mode_label": "Video 360 độ tương tác (yêu cầu WebGL): ", "preferences_category_visual": "Tùy chọn hình ảnh", "preferences_player_style_label": "Phong cách trình phát: ", @@ -82,24 +82,24 @@ "preferences_sort_label": "Sắp xếp video theo: ", "published": "được phát hành", "published - reverse": "đã xuất bản - đảo ngược", - "alphabetically": "theo thứ tự bảng chữ cái", - "alphabetically - reverse": "theo thứ tự bảng chữ cái - đảo ngược", - "channel name": "Tên kênh", - "channel name - reverse": "tên kênh - đảo ngược", + "alphabetically": "Thứ tự (A - Z)", + "alphabetically - reverse": "Thứ tự (Z - A)", + "channel name": "Tên kênh (A - Z)", + "channel name - reverse": "Tên kênh (Z - A)", "Only show latest video from channel: ": "Chỉ hiển thị video mới nhất từ kênh: ", "Only show latest unwatched video from channel: ": "Chỉ hiển thị video chưa xem mới nhất từ kênh: ", - "preferences_unseen_only_label": "Chỉ hiển thị chưa xem: ", + "preferences_unseen_only_label": "Chỉ hiển thị các video chưa từng xem: ", "preferences_notifications_only_label": "Chỉ hiển thị thông báo (nếu có): ", "Enable web notifications": "Bật thông báo web", - "`x` uploaded a video": "` x` đã tải lên một video", - "`x` is live": "` x` đang phát trực tiếp", + "`x` uploaded a video": "`x` đã tải lên một video", + "`x` is live": "`x` đang phát trực tiếp", "preferences_category_data": "Tùy chọn dữ liệu", "Clear watch history": "Xóa lịch sử xem", "Import/export data": "Nhập / xuất dữ liệu", "Change password": "Đổi mật khẩu", "Manage subscriptions": "Quản lý các mục đăng kí", "Manage tokens": "Quản lý mã thông báo", - "Watch history": "Lịch sử xem", + "Watch history": "Xem lịch sử", "Delete account": "Xóa tài khoản", "preferences_category_admin": "Tùy chọn quản trị viên", "preferences_default_home_label": "Trang chủ mặc định: ", @@ -121,7 +121,7 @@ "View privacy policy.": "Xem chính sách bảo mật.", "Trending": "Xu hướng", "Public": "Công khai", - "Unlisted": "Không hiển thị", + "Unlisted": "Không công khai", "Private": "Riêng tư", "View all playlists": "Xem tất cả danh sách phát", "Updated `x` ago": "Đã cập nhật` x` trước", @@ -131,24 +131,24 @@ "Title": "Tiêu đề", "Playlist privacy": "Bảo mật danh sách phát", "Editing playlist `x`": "Chỉnh sửa danh sách phát` x`", - "Show more": "Cho xem nhiều hơn", - "Show less": "Hiện ít hơn", + "Show more": "Hiển thị thêm", + "Show less": "Hiển thị ít hơn", "Watch on YouTube": "Xem trên YouTube", "Switch Invidious Instance": "Chuyển phiên bản Invidious", "Hide annotations": "Ẩn chú thích", "Show annotations": "Hiển thị chú thích", "Genre: ": "Thể loại: ", "License: ": "Giấy phép: ", - "Family friendly? ": "Gia đình thân thiện? ", + "Family friendly? ": "Thân thiện với gia đình? ", "Wilson score: ": "Điểm số Wilson: ", "Engagement: ": "Hôn ước: ", "Whitelisted regions: ": "Các vùng nằm trong danh sách trắng: ", - "Blacklisted regions: ": "Khu vực nằm trong danh sách đen: ", + "Blacklisted regions: ": "Các vùng nằm trong danh sách đen: ", "Shared `x`": "Chia sẻ` x`", - "View Reddit comments": "Xem nhận xét trên Reddit", - "Hide replies": "Ẩn câu trả lời", - "Show replies": "Hiển thị câu trả lời", - "Incorrect password": "Mật khẩu không đúng", + "View Reddit comments": "Xem bình luận trên Reddit", + "Hide replies": "Ẩn phản hồi", + "Show replies": "Hiển thị phản hồi", + "Incorrect password": "Mật khẩu không chính xác", "Wrong answer": "Câu trả lời sai", "Erroneous CAPTCHA": "CAPTCHA bị lỗi", "CAPTCHA is a required field": "CAPTCHA là trường bắt buộc", @@ -190,35 +190,35 @@ "Bulgarian": "Tiếng Bungari", "Burmese": "Tiếng Miến Điện", "Catalan": "Tiếng Catalan", - "Cebuano": "Cebuano", + "Cebuano": "Tiếng Cebu", "Chinese (Simplified)": "Tiếng Trung (Giản thể)", "Chinese (Traditional)": "Tiếng Trung (Phồn thể)", - "Corsican": "Corsican", + "Corsican": "Tiếng Corse", "Croatian": "Tiếng Croatia", "Czech": "Tiếng Séc", - "Danish": "Người Đan Mạch", + "Danish": "Tiếng Đan Mạch", "Dutch": "Tiếng Hà Lan", "Esperanto": "Quốc tế ngữ", "Estonian": "Tiếng Estonia", - "Filipino": "Filipino", + "Filipino": "Tiếng Philippines", "Finnish": "Tiếng Phần Lan", - "French": "Người Pháp", + "French": "Tiếng Pháp", "Galician": "Tiếng Galicia", "Georgian": "Tiếng Georgia", "German": "Tiếng Đức", - "Greek": "Người Hy Lạp", - "Gujarati": "Gujarati", - "Haitian Creole": "Tiếng Creole của Haiti", - "Hausa": "Hausa", + "Greek": "Tiếng Hy Lạp", + "Gujarati": "Tiếng Gujarat", + "Haitian Creole": "Tiếng Creole (Haiti)", + "Hausa": "Tiếng Hausa", "Hawaiian": "Tiếng Hawaii", "Hebrew": "Tiếng Do Thái", "Hindi": "Tiếng Hindi", - "Hmong": "Hmong", - "Hungarian": "Người Hungary", + "Hmong": "Tiếng Hmong", + "Hungarian": "Tiếng Hungary", "Icelandic": "Tiếng Iceland", - "Igbo": "Igbo", + "Igbo": "Tiếng Igbo", "Indonesian": "Tiếng Indonesia", - "Irish": "Tiếng Ailen", + "Irish": "Tiếng Ireland", "Italian": "Tiếng Ý", "Japanese": "Tiếng Nhật", "Javanese": "Tiếng Java", @@ -237,37 +237,37 @@ "Malagasy": "Tiếng Malagasy", "Malay": "Tiếng Mã Lai", "Malayalam": "Tiếng Malayalam", - "Maltese": "Cây nho", + "Maltese": "Tiếng Malta", "Maori": "Tiếng Maori", - "Marathi": "Marathi", + "Marathi": "Tiếng Marathi", "Mongolian": "Tiếng Mông Cổ", "Nepali": "Tiếng Nepal", - "Norwegian Bokmål": "Tiếng Na Uy Bokmål", - "Nyanja": "Nyanja", - "Pashto": "Pashto", + "Norwegian Bokmål": "Tiếng Na Uy (Bokmål)", + "Nyanja": "Tiếng Chewa / Nyanja", + "Pashto": "Tiếng Pashtun", "Persian": "Tiếng Ba Tư", - "Polish": "Đánh bóng", + "Polish": "Tiếng Ba Lan", "Portuguese": "Tiếng Bồ Đào Nha", - "Punjabi": "Punjabi", + "Punjabi": "Tiếng Punjab", "Romanian": "Tiếng Rumani", "Russian": "Tiếng Nga", - "Samoan": "Samoan", - "Scottish Gaelic": "Tiếng Gaelic Scotland", + "Samoan": "Tiếng Samoa", + "Scottish Gaelic": "Tiếng Gaelic (Scotland)", "Serbian": "Tiếng Serbia", - "Shona": "Shona", - "Sindhi": "Sindhi", - "Sinhala": "Sinhala", + "Shona": "Tiếng Shona", + "Sindhi": "Tiếng Sindh", + "Sinhala": "Tiếng Sinhala", "Slovak": "Tiếng Slovak", "Slovenian": "Tiếng Slovenia", "Somali": "Tiếng Somali", "Southern Sotho": "Southern Sotho", - "Spanish": "Người Tây Ban Nha", + "Spanish": "Tiếng Tây Ban Nha", "Spanish (Latin America)": "Tiếng Tây Ban Nha (Mỹ Latinh)", "Sundanese": "Tiếng Sundan", "Swahili": "Tiếng Swahili", "Swedish": "Tiếng Thụy Điển", - "Tajik": "Tajik", - "Tamil": "Tamil", + "Tajik": "Tiếng Tajik", + "Tamil": "Tiếng Tamil", "Telugu": "Tiếng Telugu", "Thai": "Tiếng Thái", "Turkish": "Tiếng Thổ Nhĩ Kỳ", @@ -275,17 +275,17 @@ "Urdu": "Tiếng Urdu", "Uzbek": "Tiếng Uzbek", "Vietnamese": "Tiếng Việt", - "Welsh": "Người xứ Wales", - "Western Frisian": "Western Frisian", - "Xhosa": "Xhosa", - "Yiddish": "Yiddish", - "Yoruba": "Yoruba", + "Welsh": "Tiếng Wales", + "Western Frisian": "Tiếng Tây Frisia", + "Xhosa": "Tiếng Nam Phi", + "Yiddish": "Tiếng Yiddish", + "Yoruba": "Tiếng Yoruba", "Zulu": "Tiếng Zulu", "Fallback comments: ": "Nhận xét dự phòng: ", "Popular": "Phổ biến", "Search": "Tìm kiếm", "Top": "Hàng đầu", - "About": "Trong khoảng", + "About": "Giới thiệu", "Rating: ": "Xếp hạng: ", "preferences_locale_label": "Ngôn ngữ: ", "View as playlist": "Xem dưới dạng danh sách phát", @@ -295,45 +295,45 @@ "News": "Tin tức", "Movies": "Phim", "Download": "Tải xuống", - "Download as: ": "Tải tệp dưới dạng: ", + "Download as: ": "Tải xuống dưới dạng: ", "%A %B %-d, %Y": "% A% B% -d,% Y", "(edited)": "(đã chỉnh sửa)", "YouTube comment permalink": "Liên kết cố định nhận xét trên YouTube", "permalink": "liên kết cố định", "`x` marked it with a ❤": "` x` đã đánh dấu nó bằng một ❤", - "Audio mode": "Chế độ âm thanh", - "Video mode": "Chế độ quay", + "Audio mode": "Chế độ audio", + "Video mode": "Chế độ video", "channel_tab_videos_label": "Video", "Playlists": "Danh sách phát", "channel_tab_community_label": "Cộng đồng", - "search_filters_sort_option_relevance": "liên quan", + "search_filters_sort_option_relevance": "Liên quan", "search_filters_sort_option_rating": "Xếp hạng", - "search_filters_sort_option_date": "ngày", - "search_filters_sort_option_views": "lượt xem", - "search_filters_type_label": "content_type", - "search_filters_duration_label": "thời lượng", - "search_filters_features_label": "đặc trưng", - "search_filters_sort_label": "sắp xếp", - "search_filters_date_option_hour": "giờ", - "search_filters_date_option_today": "hôm nay", - "search_filters_date_option_week": "tuần", - "search_filters_date_option_month": "tháng", - "search_filters_date_option_year": "năm", + "search_filters_sort_option_date": "Ngày tải lên", + "search_filters_sort_option_views": "Lượt xem", + "search_filters_type_label": "Thể loại", + "search_filters_duration_label": "Thời lượng", + "search_filters_features_label": "Đặc điểm", + "search_filters_sort_label": "Sắp xếp theo", + "search_filters_date_option_hour": "Một giờ qua", + "search_filters_date_option_today": "Hôm nay", + "search_filters_date_option_week": "Tuần này", + "search_filters_date_option_month": "Tháng này", + "search_filters_date_option_year": "Năm này", "search_filters_type_option_video": "video", - "search_filters_type_option_channel": "kênh", - "search_filters_type_option_playlist": "danh sách phát", - "search_filters_type_option_movie": "bộ phim", - "search_filters_type_option_show": "chỉ", - "search_filters_features_option_hd": "hd", - "search_filters_features_option_subtitles": "phụ đề", - "search_filters_features_option_c_commons": "Commons sáng tạo", - "search_filters_features_option_three_d": "3d", - "search_filters_features_option_live": "trực tiếp", - "search_filters_features_option_four_k": "4k", - "search_filters_features_option_location": "vị trí", - "search_filters_features_option_hdr": "hdr", + "search_filters_type_option_channel": "Kênh", + "search_filters_type_option_playlist": "Danh sách phát", + "search_filters_type_option_movie": "Phim", + "search_filters_type_option_show": "Hiện", + "search_filters_features_option_hd": "HD", + "search_filters_features_option_subtitles": "Phụ đề", + "search_filters_features_option_c_commons": "Giấy phép Creative Commons", + "search_filters_features_option_three_d": "3D", + "search_filters_features_option_live": "Trực tiếp", + "search_filters_features_option_four_k": "4K", + "search_filters_features_option_location": "Vị trí", + "search_filters_features_option_hdr": "HDR", "Current version: ": "Phiên bản hiện tại: ", - "search_filters_title": "bộ lọc", + "search_filters_title": "Bộ lọc", "generic_playlists_count": "{{count}} danh sách phát", "generic_views_count": "{{count}} lượt xem", "View `x` comments": { @@ -350,31 +350,31 @@ "preferences_quality_dash_label": "Chất lượng video DASH ưa thích ", "preferences_quality_dash_option_auto": "Tự động", "Subscriptions": "Thuê bao", - "View YouTube comments": "Hiển thị bình luận trên YouTube", + "View YouTube comments": "Hiển thị bình luận từ YouTube", "View more comments on Reddit": "Hiển thị thêm bình luận từ Reddit", "Music in this video": "Nhạc trong video này", "Artist: ": "Nghệ sĩ: ", "Premieres `x`": "Phát lần đầu `x`", "preferences_region_label": "Nội dung theo quốc gia ", "search_message_change_filters_or_query": "Thử mở rộng nội dung tìm kiếm hoặc thay đổi bộ lọc.", - "preferences_quality_option_small": "Nhỏ", + "preferences_quality_option_small": "Thấp", "preferences_quality_dash_option_144p": "144p", "invidious": "Invidious", "preferences_quality_dash_option_240p": "240p", - "Import/export": "Xuất/nhập dữ liệu", - "preferences_quality_dash_option_4320p": "4320p", + "Import/export": "Nhập/Xuất", + "preferences_quality_dash_option_4320p": "4320p (8K)", "preferences_quality_option_dash": "DASH (tự tối ưu chất lượng)", "generic_subscriptions_count_0": "{{count}} người đăng kí", - "preferences_quality_dash_option_1440p": "1440p", + "preferences_quality_dash_option_1440p": "1440p (2K)", "preferences_quality_dash_option_480p": "480p", - "preferences_quality_dash_option_2160p": "2160p", + "preferences_quality_dash_option_2160p": "2160p (4K)", "search_message_no_results": "Tìm kiếm không có kết quả.", "preferences_quality_dash_option_1080p": "1080p", "preferences_quality_dash_option_720p": "720p", "preferences_quality_option_medium": "Trung bình", - "Load more": "Hiển thị thêm", + "Load more": "Tải thêm", "comments_points_count_0": "{{count}} điểm", - "Import YouTube playlist (.csv)": "Nhập danh sách phát YouTube (.csv)", + "Import YouTube playlist (.csv)": "Nhập các danh sách phát từ YouTube (.csv)", "preferences_quality_dash_option_best": "Tốt nhất", "preferences_quality_dash_option_360p": "360p", "subscriptions_unseen_notifs_count_0": "{{count}} thông báo chưa đọc", @@ -382,10 +382,93 @@ "search_message_use_another_instance": " Bạn cũng có thể tìm kiếm ở một phiên bản khác.", "Standard YouTube license": "Giấy phép YouTube thông thường", "Album: ": "Album: ", - "preferences_save_player_pos_label": "Lưu vị trí xem cuối cùng ", + "preferences_save_player_pos_label": "Lưu vị trí xem: ", "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Xin chào! Có vẻ như bạn đã tắt JavaScript. Bấm vào đây để xem bình luận, lưu ý rằng thời gian tải có thể lâu hơn.", "Chinese (China)": "Tiếng Trung (Trung Quốc)", "generic_button_cancel": "Hủy", "Chinese": "Tiếng Trung", - "generic_button_delete": "Xóa" + "generic_button_delete": "Xóa", + "Korean (auto-generated)": "Tiếng Hàn (được tạo tự động)", + "search_filters_features_option_three_sixty": "360°", + "channel_tab_podcasts_label": "Podcast", + "Spanish (Mexico)": "Tiếng Tây Ban Nha (Mexico)", + "search_filters_apply_button": "Áp dụng các mục đã chọn", + "Download is disabled": "Tải xuống đã bị vô hiệu hóa.", + "next_steps_error_message_go_to_youtube": "Đi đến YouTube", + "German (auto-generated)": "Tiếng Đức (được tạo tự động)", + "Japanese (auto-generated)": "Tiếng Nhật (được tạo tự động)", + "footer_donate_page": "Ủng hộ", + "crash_page_before_reporting": "Trước khi báo cáo lỗi, hãy chắc chắn rằng bạn đã:", + "Channel Sponsor": "Nhà tài trợ của kênh", + "videoinfo_started_streaming_x_ago": "Đã bắt đầu phát sóng `x` trước", + "videoinfo_youTube_embed_link": "Nhúng", + "channel_tab_streams_label": "Phát trực tiếp", + "playlist_button_add_items": "Thêm video", + "generic_count_minutes_0": "{{count}} phút", + "user_saved_playlists": "`x` danh sách phát đã lưu", + "Spanish (Spain)": "Tiếng Tây Ban Nha (Tây Ban Nha)", + "crash_page_refresh": "Đã thử tải lại trang", + "Chinese (Hong Kong)": "Tiếng Trung (Hồng Kông)", + "generic_count_months_0": "{{count}} tháng", + "download_subtitles": "Phụ đề - `x` (.vtt)", + "generic_button_save": "Lưu", + "crash_page_search_issue": "Tìm lỗi có sẵn trên GitHub", + "none": "không", + "English (United States)": "Tiếng Anh (Mỹ)", + "next_steps_error_message_refresh": "Tải lại", + "Video unavailable": "Video không có sẵn", + "footer_source_code": "Mã nguồn", + "search_filters_duration_option_short": "Ngắn (< 4 phút)", + "search_filters_duration_option_long": "Dài (> 20 phút)", + "tokens_count_0": "{{count}} mã thông báo", + "Italian (auto-generated)": "Tiếng Ý (được tạo tự động)", + "channel_tab_shorts_label": "Shorts", + "channel_tab_releases_label": "Mới tải lên", + "`x` ago": "`x` trước", + "Interlingue": "Tiếng Khoa học Quốc tế", + "generic_channels_count_0": "{{count}} kênh", + "Chinese (Taiwan)": "Tiếng Trung (Đài Loan)", + "adminprefs_modified_source_code_url_label": "URL tới kho lưu trữ mã nguồn đã sửa đổi", + "Turkish (auto-generated)": "Tiếng Thổ Nhĩ Kỳ (được tạo tự động)", + "Indonesian (auto-generated)": "Tiếng Indonesia (được tạo tự động)", + "Portuguese (auto-generated)": "Tiếng Bồ Đào Nha (được tạo tự động)", + "generic_count_years_0": "{{count}} năm", + "videoinfo_invidious_embed_link": "Liên kết nhúng", + "Popular enabled: ": "Đã bật phổ biến: ", + "Spanish (auto-generated)": "Tiếng Tây Ban Nha (được tạo tự động)", + "English (United Kingdom)": "Tiếng Anh Anh", + "channel_tab_playlists_label": "Danh sách phát", + "generic_button_edit": "Sửa", + "search_filters_features_option_purchased": "Đã mua", + "search_filters_date_option_none": "Mọi thời điểm", + "Cantonese (Hong Kong)": "Tiếng Quảng Châu (Hồng Kông)", + "crash_page_report_issue": "Nếu các điều trên không giúp được, xin hãy tạo vấn đề mới trên GitHub (ưu tiên tiếng Anh) và đính kèm đoạn chữ sau trong nội dung (giữ nguyên KHÔNG dịch):", + "crash_page_switch_instance": "Đã thử dùng một phiên bản khác", + "generic_count_weeks_0": "{{count}} tuần", + "videoinfo_watch_on_youTube": "Xem trên YouTube", + "footer_modfied_source_code": "Mã nguồn đã chỉnh sửa", + "generic_button_rss": "RSS", + "generic_count_hours_0": "{{count}} giờ", + "French (auto-generated)": "Tiếng Pháp (được tạo tự động)", + "crash_page_read_the_faq": "Đọc Hỏi đáp thường gặp (FAQ)", + "user_created_playlists": "`x` danh sách phát đã tạo", + "channel_tab_channels_label": "Kênh", + "search_filters_type_option_all": "Mọi thể loại", + "Russian (auto-generated)": "Tiếng Nga (được tạo tự động)", + "comments_view_x_replies_0": "Xem {{count}} lượt trả lời", + "footer_original_source_code": "Mã nguồn gốc", + "Portuguese (Brazil)": "Tiếng Bồ Đào Nha (Brazil)", + "search_filters_features_option_vr180": "VR180", + "error_video_not_in_playlist": "Video không tồn tại trong danh sách phát. Bấm để trở về trang chủ của danh sách phát.", + "Dutch (auto-generated)": "Tiếng Hà Lan (được tạo tự động)", + "generic_count_days_0": "{{count}} ngày", + "Vietnamese (auto-generated)": "Tiếng Việt (được tạo tự động)", + "search_filters_duration_option_none": "Mọi thời lượng", + "footer_documentation": "Tài liệu", + "next_steps_error_message": "Bạn có thể thử: ", + "Import YouTube watch history (.json)": "Nhập lịch sử xem từ YouTube (.json)", + "search_filters_duration_option_medium": "Trung bình (4 - 20 phút)", + "generic_count_seconds_0": "{{count}} giây", + "search_filters_date_label": "Ngày tải lên", + "crash_page_you_found_a_bug": "Có vẻ như bạn đã tìm ra lỗi trong Indivious!" } From a16235d3b9537491d0b9f4a8a2c9b81323e97681 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 15 Feb 2024 18:02:14 +0100 Subject: [PATCH 519/598] Update Croatian translation Co-authored-by: Hosted Weblate Co-authored-by: Milo Ivir --- locales/hr.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/hr.json b/locales/hr.json index ef931202..2d86144f 100644 --- a/locales/hr.json +++ b/locales/hr.json @@ -503,5 +503,6 @@ "channel_tab_releases_label": "Izdanja", "generic_channels_count_0": "{{count}} kanal", "generic_channels_count_1": "{{count}} kanala", - "generic_channels_count_2": "{{count}} kanala" + "generic_channels_count_2": "{{count}} kanala", + "Import YouTube watch history (.json)": "Uvezi YouTube povijest gledanja (.json)" } From fea36fc63922d518a5982f0e6683566b28bb579b Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 15 Feb 2024 18:02:14 +0100 Subject: [PATCH 520/598] Update Hindi translation Update Hindi translation Co-authored-by: Hosted Weblate Co-authored-by: Saurmandal Co-authored-by: Snwglb --- locales/hi.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/locales/hi.json b/locales/hi.json index 21807c50..a7e0639a 100644 --- a/locales/hi.json +++ b/locales/hi.json @@ -476,7 +476,7 @@ "generic_button_cancel": "रद्द करें", "generic_button_rss": "आरएसएस", "generic_button_edit": "संपादित करें", - "generic_button_delete": "मिटाएं", + "generic_button_delete": "हटाएं", "playlist_button_add_items": "वीडियो जोड़ें", "Song: ": "गाना: ", "channel_tab_podcasts_label": "पाॅडकास्ट", @@ -484,5 +484,8 @@ "Import YouTube playlist (.csv)": "YouTube प्लेलिस्ट (.csv) आयात करें", "Standard YouTube license": "मानक यूट्यूब लाइसेंस", "Channel Sponsor": "चैनल प्रायोजक", - "Download is disabled": "डाउनलोड करना अक्षम है" + "Download is disabled": "डाउनलोड करना अक्षम है", + "generic_channels_count": "{{count}} चैनल", + "generic_channels_count_plural": "{{count}} चैनल", + "Import YouTube watch history (.json)": "YouTube पर देखने का इतिहास आयात करें (.json)" } From 3767ab2eebbd178707682cedb8b701b16527c0df Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 15 Feb 2024 18:02:14 +0100 Subject: [PATCH 521/598] Update Polish translation Update Polish translation Co-authored-by: Hosted Weblate Co-authored-by: Matthaiks --- locales/pl.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/locales/pl.json b/locales/pl.json index 313f11cb..0d18e90a 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -492,7 +492,7 @@ "Song: ": "Piosenka: ", "Channel Sponsor": "Sponsor kanału", "Standard YouTube license": "Standardowa licencja YouTube", - "Import YouTube playlist (.csv)": "Importuj playlistę YouTube (.csv)", + "Import YouTube playlist (.csv)": "Importuj playlistę z YouTube (.csv)", "generic_button_edit": "Edytuj", "generic_button_cancel": "Anuluj", "generic_button_rss": "RSS", @@ -503,5 +503,7 @@ "playlist_button_add_items": "Dodaj filmy", "generic_channels_count_0": "{{count}} kanał", "generic_channels_count_1": "{{count}} kanały", - "generic_channels_count_2": "{{count}} kanałów" + "generic_channels_count_2": "{{count}} kanałów", + "Import YouTube watch history (.json)": "Importuj historię oglądania z YouTube (.json)", + "toggle_theme": "Przełącz motyw" } From 1493e6a08635658e5c38fd48006e90d88b503082 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 15 Feb 2024 18:02:14 +0100 Subject: [PATCH 522/598] Update Italian translation Co-authored-by: Hosted Weblate Co-authored-by: Random --- locales/it.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/it.json b/locales/it.json index 7e1b12c6..7b6bb5d9 100644 --- a/locales/it.json +++ b/locales/it.json @@ -503,5 +503,6 @@ "channel_tab_podcasts_label": "Podcast", "generic_channels_count_0": "{{count}} canale", "generic_channels_count_1": "{{count}} canali", - "generic_channels_count_2": "{{count}} canali" + "generic_channels_count_2": "{{count}} canali", + "Import YouTube watch history (.json)": "Importa la cronologia delle visualizzazioni di YouTube (.json)" } From 426b472a15770411cb194a6eaf25590861855a0c Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 15 Feb 2024 18:02:15 +0100 Subject: [PATCH 523/598] Update Arabic translation Update Arabic translation Update Arabic translation Co-authored-by: Hosted Weblate Co-authored-by: Rex_sa --- locales/ar.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/locales/ar.json b/locales/ar.json index 18298913..57062e89 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -41,7 +41,7 @@ "Time (h:mm:ss):": "الوقت (h:mm:ss):", "Text CAPTCHA": "نص الكابتشا", "Image CAPTCHA": "صورة الكابتشا", - "Sign In": "تسجيل الدخول", + "Sign In": "إنشاء حساب", "Register": "التسجيل", "E-mail": "البريد الإلكتروني", "Preferences": "الإعدادات", @@ -554,5 +554,7 @@ "generic_channels_count_2": "{{count}} قناتان", "generic_channels_count_3": "{{count}} قنوات", "generic_channels_count_4": "{{count}} قنوات", - "generic_channels_count_5": "{{count}} قناة" + "generic_channels_count_5": "{{count}} قناة", + "Import YouTube watch history (.json)": "استيراد سجل مشاهدة YouTube بصيغة (.json)", + "toggle_theme": "تبديل الموضوع" } From 1d906aeeccff3d422c189fd0814e329312a4dfa5 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 15 Feb 2024 18:02:15 +0100 Subject: [PATCH 524/598] Update Interlingua translation Add Interlingua translation Co-authored-by: Hosted Weblate Co-authored-by: Software In Interlingua --- locales/ia.json | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 locales/ia.json diff --git a/locales/ia.json b/locales/ia.json new file mode 100644 index 00000000..19b6b0c0 --- /dev/null +++ b/locales/ia.json @@ -0,0 +1,41 @@ +{ + "New password": "Nove contrasigno", + "preferences_player_style_label": "Stylo de reproductor: ", + "preferences_region_label": "Pais de contento: ", + "oldest": "plus ancian", + "published": "data de publication", + "invidious": "Invidious", + "Image CAPTCHA": "Imagine CAPTCHA", + "newest": "plus nove", + "generic_button_save": "Salvar", + "Dark mode: ": "Modo obscur: ", + "preferences_dark_mode_label": "Thema: ", + "preferences_category_subscription": "Preferentias de subscription", + "last": "ultime", + "generic_button_cancel": "Cancellar", + "popular": "popular", + "Time (h:mm:ss):": "Tempore (h:mm:ss):", + "preferences_autoplay_label": "Reproduction automatic: ", + "Sign In": "Aperir le session", + "Log in": "Initiar le session", + "preferences_speed_label": "Velocitate per predefinition: ", + "preferences_comments_label": "Commentos predefinite: ", + "light": "clar", + "No": "Non", + "youtube": "YouTube", + "LIVE": "IN DIRECTE", + "reddit": "Reddit", + "preferences_category_player": "Preferentias de reproductor", + "Preferences": "Preferentias", + "preferences_quality_dash_option_auto": "Automatic", + "dark": "obscur", + "generic_button_rss": "RSS", + "Export": "Exportar", + "History": "Chronologia", + "Password": "Contrasigno", + "User ID": "ID de usator", + "E-mail": "E-mail", + "Delete account?": "Deler conto?", + "preferences_volume_label": "Volumine del reproductor: ", + "preferences_sort_label": "Ordinar le videos per: " +} From 986515dc5b7e3177e4d0dbe03ce21745b3236db2 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 15 Feb 2024 18:02:15 +0100 Subject: [PATCH 525/598] Update Indonesian translation Co-authored-by: Hosted Weblate Co-authored-by: Reza Almanda --- locales/id.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/id.json b/locales/id.json index 8961880b..4c6e8548 100644 --- a/locales/id.json +++ b/locales/id.json @@ -469,5 +469,6 @@ "error_video_not_in_playlist": "Video yang diminta tidak ada dalam daftar putar ini. Klik di sini untuk halaman beranda daftar putar.", "generic_button_delete": "Hapus", "Import YouTube playlist (.csv)": "Impor daftar putar YouTube (.csv)", - "Standard YouTube license": "Lisensi YouTube standar" + "Standard YouTube license": "Lisensi YouTube standar", + "Import YouTube watch history (.json)": "Impor riwayat tontonan YouTube (.json)" } From 1d5100462bc68047695d6888ace6e1b7a2948a50 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 15 Feb 2024 18:02:15 +0100 Subject: [PATCH 526/598] Update Dutch translation Update Dutch translation Co-authored-by: Deleted User Co-authored-by: Gert-dev Co-authored-by: Hosted Weblate --- locales/nl.json | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/locales/nl.json b/locales/nl.json index aa5da731..a30bc5b5 100644 --- a/locales/nl.json +++ b/locales/nl.json @@ -107,10 +107,10 @@ "Report statistics: ": "Statistieken bijhouden? ", "Save preferences": "Instellingen opslaan", "Subscription manager": "Abonnementen beheren", - "Token manager": "Toegangssleutels beheren", + "Token manager": "Toegangssleutelbeheerder", "Token": "Toegangssleutel", "Import/export": "Importeren/Exporteren", - "unsubscribe": "Deabonneren", + "unsubscribe": "deabonneren", "revoke": "Intrekken", "Subscriptions": "Abonnementen", "search": "zoeken", @@ -357,7 +357,7 @@ "footer_original_source_code": "Originele bron-code", "footer_modfied_source_code": "Gewijzigde bron-code", "adminprefs_modified_source_code_url_label": "URL naar gewijzigde bron-code-opslagplaats", - "next_steps_error_message": "Waarna u moet proberen om: ", + "next_steps_error_message": "Daarna moet u proberen om: ", "footer_source_code": "Bron-code", "search_filters_duration_option_long": "Lang (> 20 minuten)", "preferences_quality_option_dash": "DASH (adaptieve kwaliteit)", @@ -462,5 +462,30 @@ "Spanish (auto-generated)": "Spaans (automatisch gegenereerd)", "crash_page_you_found_a_bug": "Je lijkt een bug in Invidious tegengekomen te zijn!", "search_filters_duration_option_medium": "Gemiddeld (4 - 20 minuten)", - "crash_page_report_issue": "Indien het bovenstaande niet hielp, gelieve dan een nieuw ticket op GitHub te openen (liefst in het Engels) en neem de volgende tekst op in je bericht (gelieve deze NIET te vertalen):" + "crash_page_report_issue": "Indien het bovenstaande niet hielp, gelieve dan een nieuw ticket op GitHub te openen (liefst in het Engels) en neem de volgende tekst op in je bericht (gelieve deze NIET te vertalen):", + "channel_tab_podcasts_label": "Podcasts", + "Download is disabled": "Downloaden is uitgeschakeld", + "Channel Sponsor": "Kanaalsponsor", + "channel_tab_streams_label": "Livestreams", + "playlist_button_add_items": "Video's toevoegen", + "Artist: ": "Artiest: ", + "generic_button_save": "Opslaan", + "generic_button_cancel": "Annuleren", + "Album: ": "Album: ", + "channel_tab_shorts_label": "Shorts", + "channel_tab_releases_label": "Uitgaves", + "Song: ": "Lied: ", + "generic_channels_count": "{{count}} kanaal", + "generic_channels_count_plural": "{{count}} kanalen", + "Popular enabled: ": "Populair geactiveerd: ", + "channel_tab_playlists_label": "Afspeellijsten", + "generic_button_edit": "Bewerken", + "Music in this video": "Muziek in deze video", + "generic_button_rss": "RSS", + "channel_tab_channels_label": "Kanalen", + "error_video_not_in_playlist": "De gevraagde video bestaat niet in deze afspeellijst. Klik hier voor de startpagina van de afspeellijst.", + "generic_button_delete": "Verwijderen", + "Import YouTube playlist (.csv)": "YouTube-afspeellijst importeren (.csv)", + "Standard YouTube license": "Standaard YouTube-licentie", + "Import YouTube watch history (.json)": "YouTube-kijkgeschiedenis importeren (.json)" } From 53ce2a1a9a7aef85409c325fa95b1c7b3a9c15d9 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 15 Feb 2024 18:02:15 +0100 Subject: [PATCH 527/598] Update Spanish translation Update Spanish translation Update Spanish translation Update Spanish translation Co-authored-by: Hosted Weblate Co-authored-by: Jorge Maldonado Ventura Co-authored-by: gallegonovato --- locales/es.json | 109 ++++++++++++++++++++++++++++-------------------- 1 file changed, 64 insertions(+), 45 deletions(-) diff --git a/locales/es.json b/locales/es.json index 0b8463ea..7a41710e 100644 --- a/locales/es.json +++ b/locales/es.json @@ -90,7 +90,7 @@ "preferences_notifications_only_label": "Mostrar solo notificaciones (si hay alguna): ", "Enable web notifications": "Habilitar notificaciones web", "`x` uploaded a video": "`x` subió un video", - "`x` is live": "`x` esta en vivo", + "`x` is live": "`x` está en directo", "preferences_category_data": "Preferencias de los datos", "Clear watch history": "Borrar el historial de reproducción", "Import/export data": "Importar/Exportar datos", @@ -102,7 +102,7 @@ "preferences_category_admin": "Preferencias de administrador", "preferences_default_home_label": "Página de inicio por defecto: ", "preferences_feed_menu_label": "Menú de fuentes: ", - "preferences_show_nick_label": "Mostrar nombre de usuario arriba: ", + "preferences_show_nick_label": "Mostrar nombre de usuario encima: ", "Top enabled: ": "¿Habilitar los destacados? ", "CAPTCHA enabled: ": "¿Habilitar los CAPTCHA? ", "Login enabled: ": "¿Habilitar el inicio de sesión? ", @@ -144,13 +144,13 @@ "License: ": "Licencia: ", "Family friendly? ": "¿Filtrar contenidos? ", "Wilson score: ": "Puntuación Wilson: ", - "Engagement: ": "Compromiso: ", + "Engagement: ": "Retención: ", "Whitelisted regions: ": "Regiones permitidas: ", "Blacklisted regions: ": "Regiones bloqueadas: ", "Shared `x`": "Compartido `x`", "Premieres in `x`": "Se estrena en `x`", "Premieres `x`": "Estrenos `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.": "¡Hola! Parece que tienes JavaScript desactivado. Haz clic aquí para ver los comentarios, pero tengas en cuenta que pueden tardar un poco más en cargarse.", + "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "¡Hola! Parece que tienes JavaScript desactivado. Haz clic aquí para ver los comentarios, ten en cuenta que pueden tardar un poco más en cargar.", "View YouTube comments": "Ver los comentarios de YouTube", "View more comments on Reddit": "Ver más comentarios en Reddit", "View `x` comments": { @@ -312,7 +312,7 @@ "Download as: ": "Descargar como: ", "%A %B %-d, %Y": "%A %B %-d, %Y", "(edited)": "(editado)", - "YouTube comment permalink": "Enlace permanente de YouTube del comentario", + "YouTube comment permalink": "Enlace permanente de comentario de YouTube", "permalink": "enlace permanente", "`x` marked it with a ❤": "`x` lo ha marcado con un ❤", "Audio mode": "Modo de audio", @@ -324,10 +324,10 @@ "search_filters_sort_option_rating": "Valoración", "search_filters_sort_option_date": "Fecha de subida", "search_filters_sort_option_views": "Visualizaciones", - "search_filters_type_label": "tipo de contenido", - "search_filters_duration_label": "duración", - "search_filters_features_label": "funcionalidades", - "search_filters_sort_label": "ordenar", + "search_filters_type_label": "Tipo de contenido", + "search_filters_duration_label": "Duración", + "search_filters_features_label": "Funcionalidades", + "search_filters_sort_label": "Ordenar", "search_filters_date_option_hour": "Última hora", "search_filters_date_option_today": "Hoy", "search_filters_date_option_week": "Esta semana", @@ -390,43 +390,58 @@ "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}} visualización", - "generic_views_count_plural": "{{count}} visualizaciones", - "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}} video", - "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}} visualización", + "generic_views_count_1": "{{count}} visualizaciones", + "generic_views_count_2": "{{count}} visualizaciones", + "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 sin ver", + "subscriptions_unseen_notifs_count_1": "{{count}} notificaciones sin ver", + "subscriptions_unseen_notifs_count_2": "{{count}} notificaciones sin ver", + "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 {{count}} respuestas", + "comments_view_x_replies_2": "Ver {{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", "crash_page_search_issue": "buscado problemas existentes en GitHub", "crash_page_you_found_a_bug": "¡Parece que has encontrado un error en Invidious!", "crash_page_refresh": "probado a recargar la página", - "crash_page_report_issue": "Si nada de lo anterior ha sido de ayuda, por favor, abre una nueva incidencia en GitHub (preferiblemente en inglés) e incluye verbatim el siguiente texto en tu mensaje:", + "crash_page_report_issue": "Si nada de lo anterior ha sido de ayuda, por favor, abre una nueva incidencia en GitHub (preferiblemente en inglés) e incluye el siguiente texto en tu mensaje (NO traduzcas este texto):", "English (United States)": "Inglés (Estados Unidos)", "Cantonese (Hong Kong)": "Cantonés (Hong Kong)", "Dutch (auto-generated)": "Neerlandés (generados automáticamente)", @@ -454,14 +469,15 @@ "search_message_no_results": "No se han encontrado resultados.", "search_message_change_filters_or_query": "Pruebe ampliar la consulta de búsqueda y/o a cambiar los filtros.", "search_filters_title": "Filtros", - "search_filters_date_label": "fecha de subida", + "search_filters_date_label": "Fecha de subida", "search_filters_date_option_none": "Cualquier fecha", "search_filters_type_option_all": "Cualquier tipo", "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.", @@ -485,6 +501,9 @@ "generic_button_rss": "RSS", "channel_tab_podcasts_label": "Podcasts", "channel_tab_releases_label": "Publicaciones", - "generic_channels_count": "{{count}} canal", - "generic_channels_count_plural": "{{count}} canales" + "generic_channels_count_0": "{{count}} canal", + "generic_channels_count_1": "{{count}} canales", + "generic_channels_count_2": "{{count}} canales", + "Import YouTube watch history (.json)": "Importar el historial de las visualizaciones de YouTube (.json)", + "toggle_theme": "Alternar tema" } From aadf848ee6fd5c93004151a138bce438aabb5931 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 15 Feb 2024 18:02:15 +0100 Subject: [PATCH 528/598] Update French translation Co-authored-by: Hosted Weblate Co-authored-by: Jean Mareilles --- locales/fr.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/fr.json b/locales/fr.json index 772c81c8..251e88bc 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -503,5 +503,6 @@ "Download is disabled": "Le téléchargement est désactivé", "Import YouTube playlist (.csv)": "Importer des listes de lecture de Youtube (.csv)", "channel_tab_releases_label": "Parutions", - "channel_tab_podcasts_label": "Émissions audio" + "channel_tab_podcasts_label": "Émissions audio", + "Import YouTube watch history (.json)": "Importer l'historique de visionnement YouTube (.json)" } From 0ce945bfa8404cb9f8e6b0eb73ac1f4bf14183cf Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 15 Feb 2024 18:02:15 +0100 Subject: [PATCH 529/598] Update Swedish translation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update Swedish translation Update Swedish translation Co-authored-by: Deleted User Co-authored-by: Hosted Weblate Co-authored-by: Max Bengtzén Co-authored-by: bittin1ddc447d824349b2 --- locales/sv-SE.json | 192 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 164 insertions(+), 28 deletions(-) diff --git a/locales/sv-SE.json b/locales/sv-SE.json index a319fffd..db3486df 100644 --- a/locales/sv-SE.json +++ b/locales/sv-SE.json @@ -20,15 +20,15 @@ "No": "Nej", "Import and Export Data": "Importera och exportera data", "Import": "Importera", - "Import Invidious data": "Importera Invidious-data", - "Import YouTube subscriptions": "Importera YouTube-prenumerationer", + "Import Invidious data": "Importera Invidious JSON data", + "Import YouTube subscriptions": "Importera YouTube/OPML prenumerationer", "Import FreeTube subscriptions (.db)": "Importera FreeTube-prenumerationer (.db)", "Import NewPipe subscriptions (.json)": "Importera NewPipe-prenumerationer (.json)", "Import NewPipe data (.zip)": "Importera NewPipe-data (.zip)", "Export": "Exportera", "Export subscriptions as OPML": "Exportera prenumerationer som OPML", "Export subscriptions as OPML (for NewPipe & FreeTube)": "Exportera prenumerationer som OPML (för NewPipe och FreeTube)", - "Export data as JSON": "Exportera data som JSON", + "Export data as JSON": "Exportera Invidious data som JSON", "Delete account?": "Radera konto?", "History": "Historik", "An alternative front-end to YouTube": "Ett alternativt gränssnitt till YouTube", @@ -63,7 +63,7 @@ "preferences_related_videos_label": "Visa relaterade videor? ", "preferences_annotations_label": "Visa länkar-i-videon som förval? ", "preferences_extend_desc_label": "Förläng videobeskrivning automatiskt: ", - "preferences_vr_mode_label": "Interaktiva 360-gradervideos: ", + "preferences_vr_mode_label": "Interaktiva 360-gradervideos (kräver WebGL): ", "preferences_category_visual": "Visuella inställningar", "preferences_player_style_label": "Spelarstil: ", "Dark mode: ": "Mörkt läge: ", @@ -152,7 +152,7 @@ "View YouTube comments": "Visa YouTube-kommentarer", "View more comments on Reddit": "Visa flera kommentarer på Reddit", "View `x` comments": { - "([^.,0-9]|^)1([^.,0-9]|$)": "Visa `x` kommentarer", + "([^.,0-9]|^)1([^.,0-9]|$)": "Visa `x` kommentar", "": "Visa `x` kommentarer" }, "View Reddit comments": "Visa Reddit-kommentarer", @@ -167,7 +167,7 @@ "Wrong username or password": "Ogiltigt användarnamn eller lösenord", "Password cannot be empty": "Lösenordet kan inte vara tomt", "Password cannot be longer than 55 characters": "Lösenordet kan inte vara längre än 55 tecken", - "Please log in": "Logga in", + "Please log in": "Snälla logga in", "Invidious Private Feed for `x`": "Ogiltig privat flöde för `x`", "channel:`x`": "kanal `x`", "Deleted or invalid channel": "Raderad eller ogiltig kanal", @@ -311,8 +311,8 @@ "%A %B %-d, %Y": "%A %B %-d, %Y", "(edited)": "(redigerad)", "YouTube comment permalink": "Permanent YouTube-länk till innehållet", - "permalink": "permalänk", - "`x` marked it with a ❤": "`x` lämnade ett ❤", + "permalink": "permanent länk", + "`x` marked it with a ❤": "`x` markerade det med ett ❤", "Audio mode": "Ljudläge", "Video mode": "Videoläge", "channel_tab_videos_label": "Videor", @@ -320,30 +320,30 @@ "channel_tab_community_label": "Gemenskap", "search_filters_sort_option_relevance": "Relevans", "search_filters_sort_option_rating": "Rankning", - "search_filters_sort_option_date": "Datum", + "search_filters_sort_option_date": "Uppladdnings Datum", "search_filters_sort_option_views": "Visningar", "search_filters_type_label": "Typ", "search_filters_duration_label": "Varaktighet", "search_filters_features_label": "Funktioner", "search_filters_sort_label": "Sortera efter", - "search_filters_date_option_hour": "timme", - "search_filters_date_option_today": "idag", - "search_filters_date_option_week": "vecka", - "search_filters_date_option_month": "månad", - "search_filters_date_option_year": "år", - "search_filters_type_option_video": "video", - "search_filters_type_option_channel": "kanal", - "search_filters_type_option_playlist": "spellista", - "search_filters_type_option_movie": "film", - "search_filters_type_option_show": "tv-serie", - "search_filters_features_option_hd": "hd", - "search_filters_features_option_subtitles": "undertexter", - "search_filters_features_option_c_commons": "creative_commons", - "search_filters_features_option_three_d": "3d", - "search_filters_features_option_live": "live", - "search_filters_features_option_four_k": "4k", - "search_filters_features_option_location": "plats", - "search_filters_features_option_hdr": "hdr", + "search_filters_date_option_hour": "Senaste Timmen", + "search_filters_date_option_today": "Idag", + "search_filters_date_option_week": "Denna vecka", + "search_filters_date_option_month": "Denna månad", + "search_filters_date_option_year": "Detta år", + "search_filters_type_option_video": "Video", + "search_filters_type_option_channel": "Kanal", + "search_filters_type_option_playlist": "Spellista", + "search_filters_type_option_movie": "Film", + "search_filters_type_option_show": "Serie", + "search_filters_features_option_hd": "HD", + "search_filters_features_option_subtitles": "Undertexter/CC", + "search_filters_features_option_c_commons": "Creative Commons", + "search_filters_features_option_three_d": "3D", + "search_filters_features_option_live": "Live", + "search_filters_features_option_four_k": "4K", + "search_filters_features_option_location": "Plats", + "search_filters_features_option_hdr": "HDR", "Current version: ": "Nuvarande version: ", "next_steps_error_message_refresh": "Uppdatera", "next_steps_error_message_go_to_youtube": "Gå till Youtube", @@ -352,5 +352,141 @@ "search_filters_duration_option_long": "Lång (> 20 minuter)", "footer_documentation": "Dokumentation", "search_filters_duration_option_short": "Kort (< 4 minuter)", - "search_filters_title": "Filter" + "search_filters_title": "Filter", + "Korean (auto-generated)": "Koreanska (auto-genererad)", + "search_filters_features_option_three_sixty": "360°", + "preferences_quality_dash_option_worst": "Sämst", + "channel_tab_podcasts_label": "Podcaster", + "preferences_save_player_pos_label": "Spara uppspelningsposition: ", + "Spanish (Mexico)": "Spanska (Mexiko)", + "preferences_region_label": "Innehållsland: ", + "generic_subscriptions_count": "{{count}} prenumeration", + "generic_subscriptions_count_plural": "{{count}} prenumerationer", + "search_filters_apply_button": "Använd valda filter", + "Download is disabled": "Nedladdning är inaktiverad", + "comments_points_count": "{{count}} poäng", + "comments_points_count_plural": "{{count}} poäng", + "preferences_quality_dash_option_2160p": "2160p", + "German (auto-generated)": "Tyska (auto-genererad)", + "Japanese (auto-generated)": "Japanska (auto-genererad)", + "preferences_quality_option_medium": "Medium", + "footer_donate_page": "Donera", + "search_message_change_filters_or_query": "Prova att bredda din sökfråga och/eller ändra filtren.", + "crash_page_before_reporting": "Innan du rapporterar en bugg, se till att du har:", + "preferences_quality_dash_option_best": "Bäst", + "Channel Sponsor": "Kanal Sponsor", + "generic_videos_count": "{{count}} video", + "generic_videos_count_plural": "{{count}} videor", + "videoinfo_started_streaming_x_ago": "Började sända `x` sedan", + "videoinfo_youTube_embed_link": "Bädda in", + "channel_tab_streams_label": "Livesändningar", + "playlist_button_add_items": "Lägg till videor", + "generic_count_minutes": "{{count}}minut", + "generic_count_minutes_plural": "{{count}}minuter", + "preferences_quality_dash_option_720p": "720p", + "preferences_watch_history_label": "Aktivera visningshistorik: ", + "user_saved_playlists": "`x` sparade spellistor", + "Spanish (Spain)": "Spanska (Spanien)", + "invidious": "Invidious", + "crash_page_refresh": "försökte uppdatera sidan", + "Chinese (Hong Kong)": "Kinesiska (Hong Kong)", + "Artist: ": "Artist: ", + "generic_count_months": "{{count}}månad", + "generic_count_months_plural": "{{count}}månader", + "search_message_use_another_instance": " Du kan också söka på en annan instans.", + "generic_subscribers_count": "{{count}} prenumerant", + "generic_subscribers_count_plural": "{{count}} prenumeranter", + "download_subtitles": "Undertexter - `x` (.vtt)", + "generic_button_save": "Spara", + "crash_page_search_issue": "sökte efter befintliga problem på GitHub", + "generic_button_cancel": "Avbryt", + "none": "ingen", + "English (United States)": "English (Förenta staterna)", + "subscriptions_unseen_notifs_count": "{{count}}osedd notifikation", + "subscriptions_unseen_notifs_count_plural": "{{count}}osedda notifikationer", + "Album: ": "Album: ", + "preferences_quality_option_dash": "DASH (adaptiv kvalitet)", + "preferences_quality_dash_option_1080p": "1080p", + "Video unavailable": "Video inte tillgänglig", + "tokens_count": "{{count}}nyckel", + "tokens_count_plural": "{{count}}nycklar", + "Chinese (China)": "Kinesiska (Kina)", + "Italian (auto-generated)": "Italienska (auto-genererad)", + "channel_tab_shorts_label": "Shorts", + "preferences_quality_dash_option_1440p": "1440p", + "preferences_quality_dash_option_360p": "360p", + "search_message_no_results": "Inga resultat hittades.", + "channel_tab_releases_label": "Releaser", + "preferences_quality_dash_option_144p": "144p", + "Interlingue": "Interlingue (auto-genererad)", + "Song: ": "Låt: ", + "generic_channels_count": "{{count}} kanal", + "generic_channels_count_plural": "{{count}} kanaler", + "Chinese (Taiwan)": "Kinesiska (Taiwan)", + "preferences_quality_dash_label": "Önskad DASH-videokvalitet: ", + "adminprefs_modified_source_code_url_label": "URL till modifierad källkodslager", + "Turkish (auto-generated)": "Turkiska (auto-genererad)", + "Indonesian (auto-generated)": "Indonesiska (auto-genererad)", + "Portuguese (auto-generated)": "Portugisiska (auto-genererad)", + "generic_count_years": "{{count}}år", + "generic_count_years_plural": "{{count}}år", + "videoinfo_invidious_embed_link": "Bädda in länk", + "Popular enabled: ": "Populär aktiverad: ", + "Spanish (auto-generated)": "Spanska (auto-genererad)", + "preferences_quality_option_small": "Liten", + "English (United Kingdom)": "Engelska (Storbritannien)", + "channel_tab_playlists_label": "Spellistor", + "generic_button_edit": "Redigera", + "generic_playlists_count": "{{count}} spellista", + "generic_playlists_count_plural": "{{count}} spellistor", + "preferences_quality_option_hd720": "HD720p", + "search_filters_features_option_purchased": "Köpt", + "search_filters_date_option_none": "Vilket datum som helst", + "preferences_quality_dash_option_auto": "Auto", + "Cantonese (Hong Kong)": "Katonesiska (Hong Kong)", + "crash_page_report_issue": "Om inget av ovanstående hjälpte, vänligen öppna ett nytt nummer på GitHub (helst på engelska) och inkludera följande text i ditt meddelande (översätt INTE den texten):", + "crash_page_switch_instance": "försökte använda en annan instans", + "generic_count_weeks": "{{count}}vecka", + "generic_count_weeks_plural": "{{count}}veckor", + "videoinfo_watch_on_youTube": "Titta på YouTube", + "Music in this video": "Musik i denna video", + "footer_modfied_source_code": "Modifierad källkod", + "generic_button_rss": "RSS", + "preferences_quality_dash_option_4320p": "4320p", + "generic_count_hours": "{{count}}timme", + "generic_count_hours_plural": "{{count}}timmar", + "French (auto-generated)": "Franska (auto-genererad)", + "crash_page_read_the_faq": "läs Vanliga frågor (FAQ)", + "user_created_playlists": "`x` skapade spellistor", + "channel_tab_channels_label": "Kanaler", + "search_filters_type_option_all": "Vilken typ som helst", + "Russian (auto-generated)": "Ryska (auto-genererad)", + "preferences_quality_dash_option_480p": "480p", + "comments_view_x_replies": "Se {{count}} svar", + "comments_view_x_replies_plural": "Se {{count}} svar", + "footer_original_source_code": "Ursprunglig källkod", + "Portuguese (Brazil)": "Portugisiska (Brasilien)", + "search_filters_features_option_vr180": "VR180", + "error_video_not_in_playlist": "Den begärda videon finns inte i den här spellistan. Klicka här för startsidan för spellistan.", + "Dutch (auto-generated)": "Nederländska (auto-genererad)", + "generic_count_days": "{{count}}dag", + "generic_count_days_plural": "{{count}}dagar", + "Vietnamese (auto-generated)": "Vietnamesiska (auto-genererad)", + "search_filters_duration_option_none": "Vilken varaktighet som helst", + "preferences_quality_dash_option_240p": "240p", + "Chinese": "Kinesiska", + "preferences_automatic_instance_redirect_label": "Automatisk instansomdirigering (återgång till redirect.invidious.io): ", + "generic_button_delete": "Radera", + "Import YouTube playlist (.csv)": "Importera YouTube spellista (.csv)", + "next_steps_error_message": "Därefter bör du försöka: ", + "Standard YouTube license": "Standard YouTube licens", + "Import YouTube watch history (.json)": "Importera YouTube visningshistorik (.json)", + "search_filters_duration_option_medium": "Medium (4 - 20 minuter)", + "generic_count_seconds": "{{count}}sekund", + "generic_count_seconds_plural": "{{count}}sekunder", + "search_filters_date_label": "Uppladdningsdatum", + "crash_page_you_found_a_bug": "Det verkar som att du har hittat en bugg i Invidious!", + "generic_views_count": "{{count}} visning", + "generic_views_count_plural": "{{count}} visningar", + "toggle_theme": "Växla tema" } From 26a50eb4e857a8aa2ec280ab4c62f78783ce35b4 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 15 Feb 2024 18:02:15 +0100 Subject: [PATCH 530/598] Update Persian translation Update Persian translation Co-authored-by: Hosted Weblate Co-authored-by: Kaambiz --- locales/fa.json | 77 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 58 insertions(+), 19 deletions(-) diff --git a/locales/fa.json b/locales/fa.json index 9b6c625d..d0251201 100644 --- a/locales/fa.json +++ b/locales/fa.json @@ -1,9 +1,14 @@ { - "generic_views_count_0": "{{count}} بازدید", - "generic_videos_count_0": "{{count}} ویدئو", - "generic_playlists_count_0": "{{count}} فهرست پخش", - "generic_subscribers_count_0": "{{count}} دنبال کننده", - "generic_subscriptions_count_0": "{{count}} اشتراک ها", + "generic_views_count": "{{count}} بازدید", + "generic_views_count_plural": "{{count}} بازدید", + "generic_videos_count": "{{count}} ویدئو", + "generic_videos_count_plural": "{{count}} ویدئو", + "generic_playlists_count": "{{count}} فهرست پخش", + "generic_playlists_count_plural": "{{count}} فهرست پخش", + "generic_subscribers_count": "{{count}} دنبال کننده", + "generic_subscribers_count_plural": "{{count}} دنبال کننده", + "generic_subscriptions_count": "{{count}} اشتراک", + "generic_subscriptions_count_plural": "{{count}} اشتراک", "LIVE": "زنده", "Shared `x` ago": "`x` پیش به اشتراک گذاشته شده", "Unsubscribe": "لغو اشتراک", @@ -117,13 +122,15 @@ "Subscription manager": "مدیریت اشتراک", "Token manager": "مدیر توکن", "Token": "توکن", - "tokens_count_0": "{{count}} توکن ها", + "tokens_count": "{{count}} توکن", + "tokens_count_plural": "{{count}} توکن", "Import/export": "وارد کردن/خارج کردن", "unsubscribe": "لغو اشتراک", "revoke": "ابطال", "Subscriptions": "اشتراک ها", - "subscriptions_unseen_notifs_count_0": "{{count}} اعلان نادیده", - "search": "جستجو", + "subscriptions_unseen_notifs_count": "{{count}} اعلان نادیده", + "subscriptions_unseen_notifs_count_plural": "{{count}} اعلان نادیده", + "search": "جست و جو", "Log out": "خروج", "Released under the AGPLv3 on Github.": "منتشر شده تحت پروانه AGPLv3 روی گیت‌هاب.", "Source available here.": "منبع اینجا دردسترس است.", @@ -183,10 +190,12 @@ "This channel does not exist.": "این کانال وجود ندارد.", "Could not get channel info.": "نمیتوان اطلاعات کانال را دریافت کرد.", "Could not fetch comments": "نمیتوان نظرات را دریافت کرد", - "comments_view_x_replies_0": "نمایش {{count}} پاسخ ها", + "comments_view_x_replies": "نمایش {{count}} پاسخ", + "comments_view_x_replies_plural": "نمایش {{count}} پاسخ", "`x` ago": "`x` پیش", "Load more": "بارگذاری بیشتر", - "comments_points_count_0": "{{count}} نقطه ها", + "comments_points_count": "{{count}} نقطه", + "comments_points_count_plural": "{{count}} نقطه", "Could not create mix.": "نمیتوان میکس ساخت.", "Empty playlist": "سیاههٔ پخش خالی", "Not a playlist.": "یک سیاههٔ پخش نیست.", @@ -304,16 +313,23 @@ "Yiddish": "ییدیش", "Yoruba": "یوروبایی", "Zulu": "زولو", - "generic_count_years_0": "{{count}} سال", - "generic_count_months_0": "{{count}} ماه", - "generic_count_weeks_0": "{{count}} هفته", - "generic_count_days_0": "{{count}} روز", - "generic_count_hours_0": "{{count}} ساعت", - "generic_count_minutes_0": "{{count}} دقیقه", - "generic_count_seconds_0": "{{count}} ثانیه", + "generic_count_years": "{{count}} سال", + "generic_count_years_plural": "{{count}} سال", + "generic_count_months": "{{count}} ماه", + "generic_count_months_plural": "{{count}} ماه", + "generic_count_weeks": "{{count}} هفته", + "generic_count_weeks_plural": "{{count}} هفته", + "generic_count_days": "{{count}} روز", + "generic_count_days_plural": "{{count}} روز", + "generic_count_hours": "{{count}} ساعت", + "generic_count_hours_plural": "{{count}} ساعت", + "generic_count_minutes": "{{count}} دقیقه", + "generic_count_minutes_plural": "{{count}} دقیقه", + "generic_count_seconds": "{{count}} ثانیه", + "generic_count_seconds_plural": "{{count}} ثانیه", "Fallback comments: ": "نظرات عقب گرد: ", "Popular": "محبوب", - "Search": "جستجو", + "Search": "جست و جو", "Top": "بالا", "About": "درباره", "Rating: ": "رتبه دهی: ", @@ -445,5 +461,28 @@ "Song: ": "آهنگ: ", "Channel Sponsor": "اسپانسر کانال", "Standard YouTube license": "پروانه استاندارد YouTube", - "search_message_use_another_instance": " شما همچنین می‌توانید در نمونه دیگر هم جستجو کنید." + "search_message_use_another_instance": " شما همچنین می‌توانید در نمونه دیگر هم جستجو کنید.", + "Download is disabled": "دریافت غیرفعال است", + "crash_page_before_reporting": "پیش از گزارش ایراد، مطمئنید شوید که:", + "playlist_button_add_items": "افزودن ویدیو", + "user_saved_playlists": "فهرست‌های پخش ذخیره شده", + "crash_page_refresh": "که صفحه را بازنشانی کرده‌اید", + "generic_button_save": "ذخیره", + "generic_button_cancel": "لغو", + "generic_channels_count": "{{count}} کانال", + "generic_channels_count_plural": "{{count}} کانال", + "generic_button_edit": "ویرایش", + "crash_page_switch_instance": "که تلاش کرده‌اید از یک نمونهٔ دیگر استفاده کنید", + "generic_button_rss": "خوراک RSS", + "crash_page_read_the_faq": "که سوالات بیشتر پرسیده شده (FAQ) را خوانده‌اید", + "generic_button_delete": "حذف", + "Import YouTube playlist (.csv)": "واردکردن فهرست‌پخش YouTube (.csv)", + "Import YouTube watch history (.json)": "وارد کردن فهرست پخش YouTube (.json)", + "crash_page_you_found_a_bug": "به نظر می‌رسد که ایرادی در Invidious پیدا کرده‌اید!", + "channel_tab_podcasts_label": "پادکست‌ها", + "channel_tab_streams_label": "پخش زنده‌ها", + "channel_tab_shorts_label": "Shortها", + "channel_tab_playlists_label": "فهرست‌های پخش", + "channel_tab_channels_label": "کانال‌ها", + "error_video_not_in_playlist": "ویدیوی درخواستی معلق به این فهرست پخش نیست. کلیک کنید تا به صفحهٔ اصلی فهرست پخش بروید." } From 9688200caf508109a5ffeb43f4934defd57be85b Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 15 Feb 2024 18:02:15 +0100 Subject: [PATCH 531/598] Update Serbian translation Update Serbian translation Co-authored-by: Hosted Weblate Co-authored-by: NEXI --- locales/sr.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/locales/sr.json b/locales/sr.json index f0e5518d..6be5e03e 100644 --- a/locales/sr.json +++ b/locales/sr.json @@ -503,5 +503,7 @@ "crash_page_you_found_a_bug": "Izgleda da ste pronašli grešku u Invidious-u!", "generic_views_count_0": "{{count}} pregled", "generic_views_count_1": "{{count}} pregleda", - "generic_views_count_2": "{{count}} pregleda" + "generic_views_count_2": "{{count}} pregleda", + "Import YouTube watch history (.json)": "Uvezi YouTube istoriju gledanja (.json)", + "toggle_theme": "Укључи тему" } From e8810509c1190cf0bb33b051057b8709e8776470 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 15 Feb 2024 18:02:15 +0100 Subject: [PATCH 532/598] Update Albanian translation Update Albanian translation Co-authored-by: Besnik Bleta Co-authored-by: Hosted Weblate --- locales/sq.json | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/locales/sq.json b/locales/sq.json index 41d4161c..363a70b0 100644 --- a/locales/sq.json +++ b/locales/sq.json @@ -79,7 +79,7 @@ "invidious": "Invidious", "preferences_captions_label": "Titra parazgjedhje: ", "preferences_extend_desc_label": "Zgjero automatikisht përshkrimin e videos: ", - "preferences_player_style_label": "Silt lojtësi: ", + "preferences_player_style_label": "Stil lojtësi: ", "Dark mode: ": "Mënyra e errët: ", "preferences_dark_mode_label": "Temë: ", "dark": "e errët", @@ -477,5 +477,12 @@ "channel_tab_releases_label": "Hedhje në qarkullim", "Song: ": "Pjesë: ", "Import YouTube playlist (.csv)": "Importoni luajlistë YouTube (.csv)", - "Standard YouTube license": "Licencë YouTube standarde" + "Standard YouTube license": "Licencë YouTube standarde", + "published - reverse": "publikuar më - së prapthi", + "channel_tab_podcasts_label": "Podcast-e", + "channel name - reverse": "emër kanali - së prapthi", + "Import YouTube watch history (.json)": "Importo historik parjesh YouTube (.json)", + "preferences_local_label": "Video përmes ndërmjetësi: ", + "Fallback captions: ": "Titra nga halli: ", + "Erroneous challenge": "Zgjidhje e gabuar" } From 219b587945765765509d62e1522752cb9bd3e5ac Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 15 Feb 2024 18:02:15 +0100 Subject: [PATCH 533/598] Update Korean translation Update Korean translation Co-authored-by: Hosted Weblate Co-authored-by: simmon Co-authored-by: xrfmkrh --- locales/ko.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/locales/ko.json b/locales/ko.json index e496bd2a..c0257ee5 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -46,7 +46,7 @@ "source": "출처", "JavaScript license information": "자바스크립트 라이선스 정보", "An alternative front-end to YouTube": "유튜브의 프론트엔드 대안", - "History": "역사", + "History": "시청 기록", "Delete account?": "계정을 삭제 하시겠습니까?", "Export data as JSON": "JSON으로 데이터 내보내기", "Export subscriptions as OPML (for NewPipe & FreeTube)": "OPML로 구독 내보내기 (뉴파이프 및 프리튜브)", @@ -351,7 +351,7 @@ "News": "뉴스", "Gaming": "게임", "Music": "음악", - "Default": "디폴트", + "Default": "전체", "Rating: ": "평점: ", "About": "정보", "Top": "최고", @@ -469,5 +469,6 @@ "generic_button_cancel": "취소", "generic_button_rss": "RSS", "channel_tab_releases_label": "출시", - "generic_channels_count_0": "{{count}} 채널" + "generic_channels_count_0": "{{count}} 채널", + "Import YouTube watch history (.json)": "유튜브 시청 기록 가져오기 (.json)" } From d2ce5195593aa1b7f196f13fe1a82a1e1b00db31 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 15 Feb 2024 18:02:15 +0100 Subject: [PATCH 534/598] Update Slovenian translation Co-authored-by: Damjan Gerl Co-authored-by: Hosted Weblate --- locales/sl.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/sl.json b/locales/sl.json index 9a912f2d..3803d09c 100644 --- a/locales/sl.json +++ b/locales/sl.json @@ -520,5 +520,6 @@ "generic_channels_count_0": "{{count}} kanal", "generic_channels_count_1": "{{count}} kanala", "generic_channels_count_2": "{{count}} kanali", - "generic_channels_count_3": "{{count}} kanalov" + "generic_channels_count_3": "{{count}} kanalov", + "Import YouTube watch history (.json)": "Uvozi zgodovino gledanja YouTube (.json)" } From 8b0cbd2a292ce8641826ac4d7546df56f0424f68 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 15 Feb 2024 18:02:15 +0100 Subject: [PATCH 535/598] Update Chinese (Traditional) translation Co-authored-by: Jeff Huang --- 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 565f1d88..1520c269 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -470,5 +470,6 @@ "playlist_button_add_items": "新增影片", "channel_tab_podcasts_label": "Podcast", "channel_tab_releases_label": "發布", - "generic_channels_count_0": "{{count}} 個頻道" + "generic_channels_count_0": "{{count}} 個頻道", + "toggle_theme": "切換佈景主題" } From 8db2e060d90b888cbf27fd1be9ce48f536209655 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 15 Feb 2024 18:02:15 +0100 Subject: [PATCH 536/598] Update Chinese (Simplified) translation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Hosted Weblate Co-authored-by: 大王叫我来巡山 --- 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 db86a9bf..faa67e6c 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -470,5 +470,6 @@ "generic_button_save": "保存", "generic_button_rss": "RSS", "channel_tab_releases_label": "公告", - "generic_channels_count_0": "{{count}} 个频道" + "generic_channels_count_0": "{{count}} 个频道", + "toggle_theme": "切换主题" } From 7ff11e4c44bc7fafaafb3df9fe9c2132b25553a6 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 15 Feb 2024 18:02:15 +0100 Subject: [PATCH 537/598] Update Serbian (cyrillic) translation Update Serbian (cyrillic) translation Co-authored-by: Hosted Weblate Co-authored-by: NEXI --- locales/sr_Cyrl.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/locales/sr_Cyrl.json b/locales/sr_Cyrl.json index bf439b28..52ac4116 100644 --- a/locales/sr_Cyrl.json +++ b/locales/sr_Cyrl.json @@ -503,5 +503,7 @@ "crash_page_you_found_a_bug": "Изгледа да сте пронашли грешку у Invidious-у!", "generic_views_count_0": "{{count}} преглед", "generic_views_count_1": "{{count}} прегледа", - "generic_views_count_2": "{{count}} прегледа" + "generic_views_count_2": "{{count}} прегледа", + "Import YouTube watch history (.json)": "Увези YouTube историју гледањa (.json)", + "toggle_theme": "Укључи тему" } From 00ef004029fff0ab886ff74c9eb64739981eda59 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 15 Feb 2024 18:02:15 +0100 Subject: [PATCH 538/598] =?UTF-8?q?Update=20Norwegian=20Bokm=C3=A5l=20tran?= =?UTF-8?q?slation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Deleted User Co-authored-by: Hosted Weblate --- locales/nb-NO.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/nb-NO.json b/locales/nb-NO.json index 08b1e0e2..cf0ee286 100644 --- a/locales/nb-NO.json +++ b/locales/nb-NO.json @@ -486,5 +486,6 @@ "generic_button_rss": "RSS", "playlist_button_add_items": "Legg til videoer", "generic_channels_count": "{{count}} kanal", - "generic_channels_count_plural": "{{count}} kanaler" + "generic_channels_count_plural": "{{count}} kanaler", + "Import YouTube watch history (.json)": "Importere YouTube visningshistorikk (.json)" } From d1dddc1adc42ee384128a4814abac238d2da1d92 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Thu, 15 Feb 2024 21:36:30 +0100 Subject: [PATCH 539/598] Locales: Remove Cyrillic text from Serbian (Latin) --- locales/sr.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/locales/sr.json b/locales/sr.json index 6be5e03e..b4a98da6 100644 --- a/locales/sr.json +++ b/locales/sr.json @@ -504,6 +504,5 @@ "generic_views_count_0": "{{count}} pregled", "generic_views_count_1": "{{count}} pregleda", "generic_views_count_2": "{{count}} pregleda", - "Import YouTube watch history (.json)": "Uvezi YouTube istoriju gledanja (.json)", - "toggle_theme": "Укључи тему" + "Import YouTube watch history (.json)": "Uvezi YouTube istoriju gledanja (.json)" } From 60f6a345d926d615ff36284e9495864fc18703ef Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Thu, 15 Feb 2024 22:02:06 +0100 Subject: [PATCH 540/598] Locales: Fix broken i18Next v3/v4 plurals Languages impacted: es, fa, pt --- spec/i18next_plurals_spec.cr | 13 +++++++------ src/invidious/helpers/i18next.cr | 22 ++++++++++++---------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/spec/i18next_plurals_spec.cr b/spec/i18next_plurals_spec.cr index dab97710..dcd0f5ec 100644 --- a/spec/i18next_plurals_spec.cr +++ b/spec/i18next_plurals_spec.cr @@ -17,7 +17,7 @@ FORM_TESTS = { "cy" => I18next::Plurals::PluralForms::Special_Welsh, "fr" => I18next::Plurals::PluralForms::Special_French_Portuguese, "en" => I18next::Plurals::PluralForms::Single_not_one, - "es" => I18next::Plurals::PluralForms::Single_not_one, + "es" => I18next::Plurals::PluralForms::Special_Spanish_Italian, "ga" => I18next::Plurals::PluralForms::Special_Irish, "gd" => I18next::Plurals::PluralForms::Special_Scottish_Gaelic, "he" => I18next::Plurals::PluralForms::Special_Hebrew, @@ -33,7 +33,8 @@ FORM_TESTS = { "mt" => I18next::Plurals::PluralForms::Special_Maltese, "or" => I18next::Plurals::PluralForms::Special_Odia, "pl" => I18next::Plurals::PluralForms::Special_Polish_Kashubian, - "pt" => I18next::Plurals::PluralForms::Single_gt_one, + "pt" => I18next::Plurals::PluralForms::Special_French_Portuguese, + "pt-PT" => I18next::Plurals::PluralForms::Single_gt_one, "pt-BR" => I18next::Plurals::PluralForms::Special_French_Portuguese, "ro" => I18next::Plurals::PluralForms::Special_Romanian, "sk" => I18next::Plurals::PluralForms::Special_Czech_Slovak, @@ -77,10 +78,10 @@ SUFFIX_TESTS = { {num: 10, suffix: "_plural"}, ], "es" => [ - {num: 0, suffix: "_plural"}, - {num: 1, suffix: ""}, - {num: 10, suffix: "_plural"}, - {num: 6_000_000, suffix: "_plural"}, + {num: 0, suffix: "_2"}, + {num: 1, suffix: "_0"}, + {num: 10, suffix: "_2"}, + {num: 6_000_000, suffix: "_1"}, ], "fr" => [ {num: 0, suffix: "_0"}, diff --git a/src/invidious/helpers/i18next.cr b/src/invidious/helpers/i18next.cr index 252af6b9..9f4077e1 100644 --- a/src/invidious/helpers/i18next.cr +++ b/src/invidious/helpers/i18next.cr @@ -47,19 +47,19 @@ module I18next::Plurals private PLURAL_SETS = { PluralForms::Single_gt_one => [ - "ach", "ak", "am", "arn", "br", "fil", "gun", "ln", "mfe", "mg", - "mi", "oc", "pt", "tg", "tl", "ti", "tr", "uz", "wa", + "ach", "ak", "am", "arn", "br", "fa", "fil", "gun", "ln", "mfe", "mg", + "mi", "oc", "pt-PT", "tg", "tl", "ti", "tr", "uz", "wa", ], PluralForms::Single_not_one => [ "af", "an", "ast", "az", "bg", "bn", "ca", "da", "de", "dev", "el", "en", - "eo", "es", "et", "eu", "fi", "fo", "fur", "fy", "gl", "gu", "ha", "hi", + "eo", "et", "eu", "fi", "fo", "fur", "fy", "gl", "gu", "ha", "hi", "hu", "hy", "ia", "kk", "kn", "ku", "lb", "mai", "ml", "mn", "mr", "nah", "nap", "nb", "ne", "nl", "nn", "no", "nso", "pa", "pap", "pms", "ps", "rm", "sco", "se", "si", "so", "son", "sq", "sv", "sw", "ta", "te", "tk", "ur", "yo", ], PluralForms::None => [ - "ay", "bo", "cgg", "fa", "ht", "id", "ja", "jbo", "ka", "km", "ko", "ky", + "ay", "bo", "cgg", "ht", "id", "ja", "jbo", "ka", "km", "ko", "ky", "lo", "ms", "sah", "su", "th", "tt", "ug", "vi", "wo", "zh", ], PluralForms::Dual_Slavic => [ @@ -90,11 +90,13 @@ module I18next::Plurals "sk" => PluralForms::Special_Czech_Slovak, "sl" => PluralForms::Special_Slovenian, # Mixed v3/v4 rules - "fr" => PluralForms::Special_French_Portuguese, - "hr" => PluralForms::Special_Hungarian_Serbian, - "it" => PluralForms::Special_Spanish_Italian, - "pt-BR" => PluralForms::Special_French_Portuguese, - "sr" => PluralForms::Special_Hungarian_Serbian, + "es" => PluralForms::Special_Spanish_Italian, + "fr" => PluralForms::Special_French_Portuguese, + "hr" => PluralForms::Special_Hungarian_Serbian, + "it" => PluralForms::Special_Spanish_Italian, + "pt" => PluralForms::Special_French_Portuguese, + "pt" => PluralForms::Special_French_Portuguese, + "sr" => PluralForms::Special_Hungarian_Serbian, } # These are the v1 and v2 compatible suffixes. @@ -165,7 +167,7 @@ module I18next::Plurals def get_plural_form(locale : String) : PluralForms # Extract the ISO 639-1 or 639-2 code from an RFC 5646 language code - if !locale.matches?(/^pt-BR$/) + if !locale.matches?(/^pt-PT$/) locale = locale.split('-')[0] end From 1e6ec605e88d1874e1b8b99294312a3c51f07beb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89milien=20=28perso=29?= <4016501+unixfox@users.noreply.github.com> Date: Thu, 15 Feb 2024 22:59:00 +0100 Subject: [PATCH 541/598] Remove usage of depends_on (#4383) --- docker-compose.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 42a5c06b..7e33f6e7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,9 +36,6 @@ services: interval: 30s timeout: 5s retries: 2 - depends_on: - invidious-db: - condition: service_healthy invidious-db: image: docker.io/library/postgres:14 From ef6b766b29160e06bd9abfb864851f993e75703c Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Mon, 29 Jan 2024 21:40:25 -0500 Subject: [PATCH 542/598] Add support for multi image community posts --- assets/css/carousel.css | 119 +++++++++++++++++++++ locales/en-US.json | 5 +- src/invidious/frontend/comments_youtube.cr | 30 ++++++ src/invidious/helpers/i18n.cr | 7 +- src/invidious/views/template.ecr | 1 + 5 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 assets/css/carousel.css diff --git a/assets/css/carousel.css b/assets/css/carousel.css new file mode 100644 index 00000000..8f0906d8 --- /dev/null +++ b/assets/css/carousel.css @@ -0,0 +1,119 @@ +/* +Copyright (c) 2024 by Jennifer (https://codepen.io/jwjertzoch/pen/JjyGeRy) + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall +be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +*/ + +.carousel { + margin: 0 auto; + overflow: hidden; + text-align: center; +} + +.slides { + width: 100%; + display: flex; + overflow-x: scroll; + scrollbar-width: none; + scroll-snap-type: x mandatory; + scroll-behavior: smooth; +} + +.slides::-webkit-scrollbar { + display: none; +} + +.slides-item { + align-items: center; + border-radius: 10px; + display: flex; + flex-shrink: 0; + font-size: 100px; + height: 600px; + justify-content: center; + margin: 0 1rem; + position: relative; + scroll-snap-align: start; + transform: scale(1); + transform-origin: center center; + transition: transform .5s; + width: 100%; +} + +.carousel__nav { + padding: 1.25rem .5rem; +} + +.slider-nav { + align-items: center; + background-color: #ddd; + border-radius: 50%; + color: #000; + display: inline-flex; + height: 1.5rem; + justify-content: center; + padding: .5rem; + position: relative; + text-decoration: none; + width: 1.5rem; +} + +.skip-link { + height: 1px; + overflow: hidden; + position: absolute; + top: auto; + width: 1px; +} + +.skip-link:focus { + align-items: center; + background-color: #000; + color: #fff; + display: flex; + font-size: 30px; + height: 30px; + justify-content: center; + opacity: .8; + text-decoration: none; + width: 50%; + z-index: 1; +} + +.light-theme .slider-nav { + background-color: #ddd; +} + +.dark-theme .slider-nav { + background-color: #0005; +} + +@media (prefers-color-scheme: light) { + .no-theme .slider-nav { + background-color: #ddd; + } +} + +@media (prefers-color-scheme: dark) { + .no-theme .slider-nav { + background-color: #0005; + } +} \ No newline at end of file diff --git a/locales/en-US.json b/locales/en-US.json index 227b0677..7899ba0a 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -488,5 +488,8 @@ "channel_tab_playlists_label": "Playlists", "channel_tab_community_label": "Community", "channel_tab_channels_label": "Channels", - "toggle_theme": "Toggle Theme" + "toggle_theme": "Toggle Theme", + "carousel_slide": "Slide {{current}} of {{total}}", + "carousel_skip": "Skip the Carousel", + "carousel_go_to": "Go to slide `x`" } diff --git a/src/invidious/frontend/comments_youtube.cr b/src/invidious/frontend/comments_youtube.cr index ecc0bc1b..6551d411 100644 --- a/src/invidious/frontend/comments_youtube.cr +++ b/src/invidious/frontend/comments_youtube.cr @@ -107,6 +107,36 @@ module Invidious::Frontend::Comments
    END_HTML end + when "multiImage" + html << <<-END_HTML + + END_HTML else nil # Ignore end end diff --git a/src/invidious/helpers/i18n.cr b/src/invidious/helpers/i18n.cr index 76e477a4..8e2f7f44 100644 --- a/src/invidious/helpers/i18n.cr +++ b/src/invidious/helpers/i18n.cr @@ -78,7 +78,7 @@ def load_all_locales return locales end -def translate(locale : String?, key : String, text : String | Nil = nil) : String +def translate(locale : String?, key : String, text : String | Nil = nil, texts : Hash(String, String) | Nil = nil) : String # Log a warning if "key" doesn't exist in en-US locale and return # that key as the text, so this is more or less transparent to the user. if !LOCALES["en-US"].has_key?(key) @@ -116,6 +116,11 @@ def translate(locale : String?, key : String, text : String | Nil = nil) : Strin if text translation = translation.gsub("`x`", text) + elsif texts + # adds support for multi string interpolation. Based on i18next https://www.i18next.com/translation-function/interpolation#basic + texts.each_key do |hash_key| + translation = translation.gsub("{{#{hash_key}}}", texts[hash_key]) + end end return translation diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr index fd755619..379cf779 100644 --- a/src/invidious/views/template.ecr +++ b/src/invidious/views/template.ecr @@ -21,6 +21,7 @@ + From 26429bee3f2bede1d4270f6e71a52482be1d5d49 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Thu, 15 Feb 2024 21:44:40 -0500 Subject: [PATCH 543/598] make it so interpolation text can be a hash Co-Authored-By: Samantaz Fox --- src/invidious/frontend/comments_youtube.cr | 2 +- src/invidious/helpers/i18n.cr | 20 +++++++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/invidious/frontend/comments_youtube.cr b/src/invidious/frontend/comments_youtube.cr index 6551d411..aecac87f 100644 --- a/src/invidious/frontend/comments_youtube.cr +++ b/src/invidious/frontend/comments_youtube.cr @@ -117,7 +117,7 @@ module Invidious::Frontend::Comments image_array.each_index do |i| html << <<-END_HTML -
    (i + 1).to_s, "total" => image_array.size.to_s})}" tabindex="0"> +
    (i + 1).to_s, "total" => image_array.size.to_s})}" tabindex="0">
    END_HTML diff --git a/src/invidious/helpers/i18n.cr b/src/invidious/helpers/i18n.cr index 8e2f7f44..23a1aafc 100644 --- a/src/invidious/helpers/i18n.cr +++ b/src/invidious/helpers/i18n.cr @@ -78,7 +78,7 @@ def load_all_locales return locales end -def translate(locale : String?, key : String, text : String | Nil = nil, texts : Hash(String, String) | Nil = nil) : String +def translate(locale : String?, key : String, text : String | Hash(String, String) | Nil = nil) : String # Log a warning if "key" doesn't exist in en-US locale and return # that key as the text, so this is more or less transparent to the user. if !LOCALES["en-US"].has_key?(key) @@ -101,10 +101,12 @@ def translate(locale : String?, key : String, text : String | Nil = nil, texts : match_length = 0 raw_data.as_h.each do |hash_key, value| - if md = text.try &.match(/#{hash_key}/) - if md[0].size >= match_length - translation = value.as_s - match_length = md[0].size + if text.is_a?(String) + if md = text.try &.match(/#{hash_key}/) + if md[0].size >= match_length + translation = value.as_s + match_length = md[0].size + end end end end @@ -114,12 +116,12 @@ def translate(locale : String?, key : String, text : String | Nil = nil, texts : raise "Invalid translation \"#{raw_data}\"" end - if text + if text.is_a?(String) translation = translation.gsub("`x`", text) - elsif texts + elsif text.is_a?(Hash(String, String)) # adds support for multi string interpolation. Based on i18next https://www.i18next.com/translation-function/interpolation#basic - texts.each_key do |hash_key| - translation = translation.gsub("{{#{hash_key}}}", texts[hash_key]) + text.each_key do |hash_key| + translation = translation.gsub("{{#{hash_key}}}", text[hash_key]) end end From a957b0fb7c517193dc9b20e7724feb46fe23912e Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Fri, 16 Feb 2024 16:22:43 -0500 Subject: [PATCH 544/598] remove trailing white spaces --- assets/css/carousel.css | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/assets/css/carousel.css b/assets/css/carousel.css index 8f0906d8..4bae92e5 100644 --- a/assets/css/carousel.css +++ b/assets/css/carousel.css @@ -1,24 +1,24 @@ /* Copyright (c) 2024 by Jennifer (https://codepen.io/jwjertzoch/pen/JjyGeRy) -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, - including without limitation the rights to use, copy, modify, -merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is + including without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ @@ -36,7 +36,7 @@ DEALINGS IN THE SOFTWARE. scroll-snap-type: x mandatory; scroll-behavior: smooth; } - + .slides::-webkit-scrollbar { display: none; } @@ -116,4 +116,4 @@ DEALINGS IN THE SOFTWARE. .no-theme .slider-nav { background-color: #0005; } -} \ No newline at end of file +} From 5ceeefa2362de32b97ca2d5c8b44e8d87f3e0ba9 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Fri, 1 Mar 2024 23:31:55 -0500 Subject: [PATCH 545/598] add support for new likes format --- src/invidious/videos/parser.cr | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr index 77520dbe..c9aea47b 100644 --- a/src/invidious/videos/parser.cr +++ b/src/invidious/videos/parser.cr @@ -266,7 +266,18 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any .try &.dig?("videoActions", "menuRenderer", "topLevelButtons") if toplevel_buttons - likes_button = toplevel_buttons.try &.as_a + # New Format as of december 2023 + likes_button = toplevel_buttons.dig?(0, + "segmentedLikeDislikeButtonViewModel", + "likeButtonViewModel", + "likeButtonViewModel", + "toggleButtonViewModel", + "toggleButtonViewModel", + "defaultButtonViewModel", + "buttonViewModel" + ) + + likes_button ||= toplevel_buttons.try &.as_a .find(&.dig?("toggleButtonRenderer", "defaultIcon", "iconType").=== "LIKE") .try &.["toggleButtonRenderer"] @@ -279,9 +290,10 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any ) if likes_button + likes_txt = likes_button.dig?("accessibilityText") # Note: The like count from `toggledText` is off by one, as it would # represent the new like count in the event where the user clicks on "like". - likes_txt = (likes_button["defaultText"]? || likes_button["toggledText"]?) + likes_txt ||= (likes_button["defaultText"]? || likes_button["toggledText"]?) .try &.dig?("accessibility", "accessibilityData", "label") likes = likes_txt.as_s.gsub(/\D/, "").to_i64? if likes_txt From 619aa3ff050573c119d9acf8302a4ddeb2beddc0 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Wed, 6 Mar 2024 20:46:50 +0100 Subject: [PATCH 546/598] YoutubeAPI: bump client versions --- src/invidious/yt_backend/youtube_api.cr | 33 +++++++++++++------------ 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/invidious/yt_backend/youtube_api.cr b/src/invidious/yt_backend/youtube_api.cr index a5e621f2..9e0631f6 100644 --- a/src/invidious/yt_backend/youtube_api.cr +++ b/src/invidious/yt_backend/youtube_api.cr @@ -7,17 +7,18 @@ module YoutubeAPI private DEFAULT_API_KEY = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8" - private ANDROID_APP_VERSION = "18.20.38" - # github.com/TeamNewPipe/NewPipeExtractor/blob/943b7c033bb9d07ead63ddab4441c287653e4384/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java#L1308 - private ANDROID_USER_AGENT = "com.google.android.youtube/18.20.38 (Linux; U; Android 12; US) gzip" + # For Android versions, see https://en.wikipedia.org/wiki/Android_version_history + private ANDROID_APP_VERSION = "19.09.36" + private ANDROID_USER_AGENT = "com.google.android.youtube/19.09.36 (Linux; U; Android 12; US) gzip" private ANDROID_SDK_VERSION = 31_i64 private ANDROID_VERSION = "12" - private IOS_APP_VERSION = "18.21.3" - # github.com/TeamNewPipe/NewPipeExtractor/blob/943b7c033bb9d07ead63ddab4441c287653e4384/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java#L1330 - private IOS_USER_AGENT = "com.google.ios.youtube/18.21.3 (iPhone14,5; U; CPU iOS 15_6 like Mac OS X;)" - # github.com/TeamNewPipe/NewPipeExtractor/blob/943b7c033bb9d07ead63ddab4441c287653e4384/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java#L1224 - private IOS_VERSION = "15.6.0.19G71" + # For Apple device names, see https://gist.github.com/adamawolf/3048717 + # For iOS versions, see https://en.wikipedia.org/wiki/IOS_version_history#Releases, + # then go to the dedicated article of the major version you want. + private IOS_APP_VERSION = "19.09.3" + private IOS_USER_AGENT = "com.google.ios.youtube/19.09.3 (iPhone14,5; U; CPU iOS 17_4 like Mac OS X;)" + private IOS_VERSION = "17.4.0.21E219" # Major.Minor.Patch.Build private WINDOWS_VERSION = "10.0" @@ -45,7 +46,7 @@ module YoutubeAPI ClientType::Web => { name: "WEB", name_proto: "1", - version: "2.20230602.01.00", + version: "2.20240304.00.00", api_key: DEFAULT_API_KEY, screen: "WATCH_FULL_SCREEN", os_name: "Windows", @@ -55,7 +56,7 @@ module YoutubeAPI ClientType::WebEmbeddedPlayer => { name: "WEB_EMBEDDED_PLAYER", name_proto: "56", - version: "1.20220803.01.00", + version: "1.20240303.00.00", api_key: DEFAULT_API_KEY, screen: "EMBED", os_name: "Windows", @@ -65,7 +66,7 @@ module YoutubeAPI ClientType::WebMobile => { name: "MWEB", name_proto: "2", - version: "2.20230531.05.00", + version: "2.20240304.08.00", api_key: DEFAULT_API_KEY, os_name: "Android", os_version: ANDROID_VERSION, @@ -74,7 +75,7 @@ module YoutubeAPI ClientType::WebScreenEmbed => { name: "WEB", name_proto: "1", - version: "2.20220804.00.00", + version: "2.20240304.00.00", api_key: DEFAULT_API_KEY, screen: "EMBED", os_name: "Windows", @@ -99,7 +100,7 @@ module YoutubeAPI name: "ANDROID_EMBEDDED_PLAYER", name_proto: "55", version: ANDROID_APP_VERSION, - api_key: DEFAULT_API_KEY, + api_key: "AIzaSyCjc_pVEDi4qsv5MtC2dMXzpIaDoRFLsxw", }, ClientType::AndroidScreenEmbed => { name: "ANDROID", @@ -143,9 +144,9 @@ module YoutubeAPI ClientType::IOSMusic => { name: "IOS_MUSIC", name_proto: "26", - version: "5.21", + version: "6.42", api_key: "AIzaSyBAETezhkwP0ZWA02RsqT1zu78Fpt0bC_s", - user_agent: "com.google.ios.youtubemusic/5.21 (iPhone14,5; U; CPU iOS 15_6 like Mac OS X;)", + user_agent: "com.google.ios.youtubemusic/6.42 (iPhone14,5; U; CPU iOS 17_4 like Mac OS X;)", device_make: "Apple", device_model: "iPhone14,5", os_name: "iPhone", @@ -158,7 +159,7 @@ module YoutubeAPI ClientType::TvHtml5 => { name: "TVHTML5", name_proto: "7", - version: "7.20220325", + version: "7.20240304.10.00", api_key: DEFAULT_API_KEY, }, ClientType::TvHtml5ScreenEmbed => { From 0aaa3e6a08ae80f272ad260dc61213c8af83894b Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Sun, 26 Nov 2023 17:34:30 -0500 Subject: [PATCH 547/598] API: Parse channel's tags --- src/invidious/channels/about.cr | 16 ++++++++++++++-- src/invidious/routes/api/v1/channels.cr | 1 + 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/invidious/channels/about.cr b/src/invidious/channels/about.cr index 8b60a728..b5a27667 100644 --- a/src/invidious/channels/about.cr +++ b/src/invidious/channels/about.cr @@ -14,6 +14,7 @@ record AboutChannel, is_family_friendly : Bool, allowed_regions : Array(String), tabs : Array(String), + tags : Array(String), verified : Bool def get_about_info(ucid, locale) : AboutChannel @@ -43,6 +44,8 @@ def get_about_info(ucid, locale) : AboutChannel auto_generated = true end + tags = [] of String + if auto_generated author = initdata["header"]["interactiveTabbedHeaderRenderer"]["title"]["simpleText"].as_s author_url = initdata["microformat"]["microformatDataRenderer"]["urlCanonical"].as_s @@ -52,7 +55,13 @@ def get_about_info(ucid, locale) : AboutChannel banners = initdata["header"]["interactiveTabbedHeaderRenderer"]?.try &.["banner"]?.try &.["thumbnails"]? banner = banners.try &.[-1]?.try &.["url"].as_s? - description_node = initdata["header"]["interactiveTabbedHeaderRenderer"]["description"] + description_base_node = initdata["header"]["interactiveTabbedHeaderRenderer"]["description"] + # some channels have the description in a simpleText + # ex: https://www.youtube.com/channel/UCQvWX73GQygcwXOTSf_VDVg/ + description_node = description_base_node.dig?("simpleText") || description_base_node + + tags = initdata.dig?("header", "interactiveTabbedHeaderRenderer", "badges") + .try &.as_a.map(&.["metadataBadgeRenderer"]["label"].as_s) || [] of String else author = initdata["metadata"]["channelMetadataRenderer"]["title"].as_s author_url = initdata["metadata"]["channelMetadataRenderer"]["channelUrl"].as_s @@ -70,6 +79,7 @@ def get_about_info(ucid, locale) : AboutChannel # end description_node = initdata["metadata"]["channelMetadataRenderer"]?.try &.["description"]? + tags = initdata.dig?("microformat", "microformatDataRenderer", "tags").try &.as_a.map(&.as_s) || [] of String end is_family_friendly = initdata["microformat"]["microformatDataRenderer"]["familySafe"].as_bool @@ -131,7 +141,8 @@ def get_about_info(ucid, locale) : AboutChannel # ["description"]["simpleText"] and ["primaryLinks"][0]["title"]["simpleText"] auto_generated = ( (channel_about_meta["primaryLinks"]?.try &.size) == 1 && \ - extract_text(channel_about_meta.dig?("primaryLinks", 0, "title")) == "Auto-generated by YouTube" + extract_text(channel_about_meta.dig?("primaryLinks", 0, "title")) == "Auto-generated by YouTube" || + channel_about_meta.dig?("links", 0, "channelExternalLinkViewModel", "title", "content").try &.as_s == "Auto-generated by YouTube" ) end end @@ -155,6 +166,7 @@ def get_about_info(ucid, locale) : AboutChannel is_family_friendly: is_family_friendly, allowed_regions: allowed_regions, tabs: tab_names, + tags: tags, verified: author_verified || false, ) end diff --git a/src/invidious/routes/api/v1/channels.cr b/src/invidious/routes/api/v1/channels.cr index 67018660..1d409c79 100644 --- a/src/invidious/routes/api/v1/channels.cr +++ b/src/invidious/routes/api/v1/channels.cr @@ -90,6 +90,7 @@ module Invidious::Routes::API::V1::Channels json.field "allowedRegions", channel.allowed_regions json.field "tabs", channel.tabs + json.field "tags", channel.tags json.field "authorVerified", channel.verified json.field "latestVideos" do From 1a2d408d38fd0baef9a5538f3971fb7ac9abd147 Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Sun, 31 Mar 2024 11:37:13 -0400 Subject: [PATCH 548/598] Update shorts params --- src/invidious/videos/parser.cr | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr index 77520dbe..75fe4a36 100644 --- a/src/invidious/videos/parser.cr +++ b/src/invidious/videos/parser.cr @@ -142,8 +142,9 @@ end def try_fetch_streaming_data(id : String, client_config : YoutubeAPI::ClientConfig) : Hash(String, JSON::Any)? LOGGER.debug("try_fetch_streaming_data: [#{id}] Using #{client_config.client_type} client.") - # 2AMBCgIQBg is a workaround for streaming URLs that returns a 403. - response = YoutubeAPI.player(video_id: id, params: "2AMBCgIQBg", client_config: client_config) + # CgIIAdgDAQ%3D%3D is a workaround for streaming URLs that returns a 403. + # https://github.com/LuanRT/YouTube.js/pull/624 + response = YoutubeAPI.player(video_id: id, params: "CgIIAdgDAQ%3D%3D", client_config: client_config) playability_status = response["playabilityStatus"]["status"] LOGGER.debug("try_fetch_streaming_data: [#{id}] Got playabilityStatus == #{playability_status}.") From 170eef58fd907057782244c95f9b8d72bb85d114 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Thu, 4 Apr 2024 19:10:27 -0400 Subject: [PATCH 549/598] Use trending api for health checks --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 7e33f6e7..afda8726 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -32,7 +32,7 @@ services: # statistics_enabled: false hmac_key: "CHANGE_ME!!" healthcheck: - test: wget -nv --tries=1 --spider http://127.0.0.1:3000/api/v1/comments/jNQXAC9IVRw || exit 1 + test: wget -nv --tries=1 --spider http://127.0.0.1:3000/api/v1/trending || exit 1 interval: 30s timeout: 5s retries: 2 From 2a029b4d8c8e5f1c0d34ae5ab48a8c3624d67012 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Thu, 4 Apr 2024 20:20:27 -0400 Subject: [PATCH 550/598] Add field for `authorVerified` for recommended videos when using the API --- src/invidious/jsonify/api_v1/video_json.cr | 1 + 1 file changed, 1 insertion(+) diff --git a/src/invidious/jsonify/api_v1/video_json.cr b/src/invidious/jsonify/api_v1/video_json.cr index 1651559a..ed912ff3 100644 --- a/src/invidious/jsonify/api_v1/video_json.cr +++ b/src/invidious/jsonify/api_v1/video_json.cr @@ -227,6 +227,7 @@ module Invidious::JSONify::APIv1 json.field "author", rv["author"] json.field "authorUrl", "/channel/#{rv["ucid"]?}" json.field "authorId", rv["ucid"]? + json.field "authorVerified", rv["author_verified"] if rv["author_thumbnail"]? json.field "authorThumbnails" do json.array do From bfd9c9876e2fc31d7e61fc298e31e7da75f35c87 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Sun, 7 Apr 2024 10:26:33 -0400 Subject: [PATCH 551/598] Parse if video is post live dvr and include it in API --- src/invidious/jsonify/api_v1/video_json.cr | 1 + src/invidious/videos.cr | 4 ++++ src/invidious/videos/parser.cr | 4 ++++ 3 files changed, 9 insertions(+) diff --git a/src/invidious/jsonify/api_v1/video_json.cr b/src/invidious/jsonify/api_v1/video_json.cr index 1651559a..705210ab 100644 --- a/src/invidious/jsonify/api_v1/video_json.cr +++ b/src/invidious/jsonify/api_v1/video_json.cr @@ -62,6 +62,7 @@ module Invidious::JSONify::APIv1 json.field "rating", 0_i64 json.field "isListed", video.is_listed json.field "liveNow", video.live_now + json.field "isPostLiveDvr", video.post_live_dvr json.field "isUpcoming", video.is_upcoming if video.premiere_timestamp diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index a8f02056..2f44939c 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -82,6 +82,10 @@ struct Video return (self.video_type == VideoType::Livestream) end + def post_live_dvr + return info["isPostLiveDvr"].as_bool + end + def premiere_timestamp : Time? info .dig?("microformat", "playerMicroformatRenderer", "liveBroadcastDetails", "startTimestamp") diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr index 75fe4a36..63e46701 100644 --- a/src/invidious/videos/parser.cr +++ b/src/invidious/videos/parser.cr @@ -216,6 +216,9 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any live_now = microformat.dig?("liveBroadcastDetails", "isLiveNow") .try &.as_bool || false + post_live_dvr = video_details.dig?("isPostLiveDvr") + .try &.as_bool || false + # Extra video infos allowed_regions = microformat["availableCountries"]? @@ -405,6 +408,7 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any "isListed" => JSON::Any.new(is_listed || false), "isUpcoming" => JSON::Any.new(is_upcoming || false), "keywords" => JSON::Any.new(keywords.map { |v| JSON::Any.new(v) }), + "isPostLiveDvr" => JSON::Any.new(post_live_dvr), # Related videos "relatedVideos" => JSON::Any.new(related), # Description From 990931ff67098405066606dd62b5b9b085f3f64d Mon Sep 17 00:00:00 2001 From: syeopite Date: Sun, 7 Apr 2024 11:08:12 -0700 Subject: [PATCH 552/598] Remove legacy proxy code --- src/invidious/videos.cr | 11 - src/invidious/videos/parser.cr | 4 +- src/invidious/yt_backend/connection_pool.cr | 44 +-- src/invidious/yt_backend/proxy.cr | 316 -------------------- src/invidious/yt_backend/youtube_api.cr | 18 +- 5 files changed, 20 insertions(+), 373 deletions(-) delete mode 100644 src/invidious/yt_backend/proxy.cr diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index a8f02056..148a8636 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -394,17 +394,6 @@ def fetch_video(id, region) .dig?("microformat", "playerMicroformatRenderer", "availableCountries") .try &.as_a.map &.as_s || [] of String - # Check for region-blocks - if info["reason"]?.try &.as_s.includes?("your country") - bypass_regions = PROXY_LIST.keys & allowed_regions - if !bypass_regions.empty? - region = bypass_regions[rand(bypass_regions.size)] - region_info = extract_video_info(video_id: id, proxy_region: region) - region_info["region"] = JSON::Any.new(region) if region - info = region_info if !region_info["reason"]? - end - end - if reason = info["reason"]? if reason == "Video unavailable" raise NotFoundException.new(reason.as_s || "") diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr index 75fe4a36..4cde08c4 100644 --- a/src/invidious/videos/parser.cr +++ b/src/invidious/videos/parser.cr @@ -50,9 +50,9 @@ def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)? } end -def extract_video_info(video_id : String, proxy_region : String? = nil) +def extract_video_info(video_id : String) # Init client config for the API - client_config = YoutubeAPI::ClientConfig.new(proxy_region: proxy_region) + client_config = YoutubeAPI::ClientConfig.new # Fetch data from the player endpoint player_response = YoutubeAPI.player(video_id: video_id, params: "", client_config: client_config) diff --git a/src/invidious/yt_backend/connection_pool.cr b/src/invidious/yt_backend/connection_pool.cr index 81cfb272..d3dbcc0e 100644 --- a/src/invidious/yt_backend/connection_pool.cr +++ b/src/invidious/yt_backend/connection_pool.cr @@ -24,25 +24,20 @@ struct YoutubeConnectionPool @pool = build_pool() end - def client(region = nil, &block) - if region - conn = make_client(url, region, force_resolve = true) + def client(&block) + conn = pool.checkout + begin response = yield conn - else - conn = pool.checkout - begin - response = yield conn - rescue ex - conn.close - conn = HTTP::Client.new(url) + rescue ex + conn.close + conn = HTTP::Client.new(url) - conn.family = CONFIG.force_resolve - conn.family = Socket::Family::INET if conn.family == Socket::Family::UNSPEC - conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com" - response = yield conn - ensure - pool.release(conn) - end + conn.family = CONFIG.force_resolve + conn.family = Socket::Family::INET if conn.family == Socket::Family::UNSPEC + conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com" + response = yield conn + ensure + pool.release(conn) end response @@ -60,9 +55,9 @@ struct YoutubeConnectionPool end def make_client(url : URI, region = nil, force_resolve : Bool = false) - client = HTTPClient.new(url, OpenSSL::SSL::Context::Client.insecure) + client = HTTP::Client.new(url) - # Some services do not support IPv6. + # Force the usage of a specific configured IP Family if force_resolve client.family = CONFIG.force_resolve end @@ -71,17 +66,6 @@ def make_client(url : URI, region = nil, force_resolve : Bool = false) client.read_timeout = 10.seconds client.connect_timeout = 10.seconds - if region - PROXY_LIST[region]?.try &.sample(40).each do |proxy| - begin - proxy = HTTPProxy.new(proxy_host: proxy[:ip], proxy_port: proxy[:port]) - client.set_proxy(proxy) - break - rescue ex - end - end - end - return client end diff --git a/src/invidious/yt_backend/proxy.cr b/src/invidious/yt_backend/proxy.cr deleted file mode 100644 index 2d0fd4ba..00000000 --- a/src/invidious/yt_backend/proxy.cr +++ /dev/null @@ -1,316 +0,0 @@ -# See https://github.com/crystal-lang/crystal/issues/2963 -class HTTPProxy - getter proxy_host : String - getter proxy_port : Int32 - getter options : Hash(Symbol, String) - getter tls : OpenSSL::SSL::Context::Client? - - def initialize(@proxy_host, @proxy_port = 80, @options = {} of Symbol => String) - end - - def open(host, port, tls = nil, connection_options = {} of Symbol => Float64 | Nil) - dns_timeout = connection_options.fetch(:dns_timeout, nil) - connect_timeout = connection_options.fetch(:connect_timeout, nil) - read_timeout = connection_options.fetch(:read_timeout, nil) - - socket = TCPSocket.new @proxy_host, @proxy_port, dns_timeout, connect_timeout - socket.read_timeout = read_timeout if read_timeout - socket.sync = true - - socket << "CONNECT #{host}:#{port} HTTP/1.1\r\n" - - if options[:user]? - credentials = Base64.strict_encode("#{options[:user]}:#{options[:password]}") - credentials = "#{credentials}\n".gsub(/\s/, "") - socket << "Proxy-Authorization: Basic #{credentials}\r\n" - end - - socket << "\r\n" - - resp = parse_response(socket) - - if resp[:code]? == 200 - {% if !flag?(:without_openssl) %} - if tls - tls_socket = OpenSSL::SSL::Socket::Client.new(socket, context: tls, sync_close: true, hostname: host) - socket = tls_socket - end - {% end %} - - return socket - else - socket.close - raise IO::Error.new(resp.inspect) - end - end - - private def parse_response(socket) - resp = {} of Symbol => Int32 | String | Hash(String, String) - - begin - version, code, reason = socket.gets.as(String).chomp.split(/ /, 3) - - headers = {} of String => String - - while (line = socket.gets.as(String)) && (line.chomp != "") - name, value = line.split(/:/, 2) - headers[name.strip] = value.strip - end - - resp[:version] = version - resp[:code] = code.to_i - resp[:reason] = reason - resp[:headers] = headers - rescue - end - - return resp - end -end - -class HTTPClient < HTTP::Client - def set_proxy(proxy : HTTPProxy) - begin - @io = proxy.open(host: @host, port: @port, tls: @tls, connection_options: proxy_connection_options) - rescue IO::Error - @io = nil - end - end - - def unset_proxy - @io = nil - end - - def proxy_connection_options - opts = {} of Symbol => Float64 | Nil - - opts[:dns_timeout] = @dns_timeout - opts[:connect_timeout] = @connect_timeout - opts[:read_timeout] = @read_timeout - - return opts - end -end - -def get_proxies(country_code = "US") - # return get_spys_proxies(country_code) - return get_nova_proxies(country_code) -end - -def filter_proxies(proxies) - proxies.select! do |proxy| - begin - client = HTTPClient.new(YT_URL) - client.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com" - client.read_timeout = 10.seconds - client.connect_timeout = 10.seconds - - proxy = HTTPProxy.new(proxy_host: proxy[:ip], proxy_port: proxy[:port]) - client.set_proxy(proxy) - - status_ok = client.head("/").status_code == 200 - client.close - status_ok - rescue ex - false - end - end - - return proxies -end - -def get_nova_proxies(country_code = "US") - country_code = country_code.downcase - client = HTTP::Client.new(URI.parse("https://www.proxynova.com")) - client.read_timeout = 10.seconds - client.connect_timeout = 10.seconds - - headers = HTTP::Headers.new - headers["User-Agent"] = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36" - headers["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8" - headers["Accept-Language"] = "Accept-Language: en-US,en;q=0.9" - headers["Host"] = "www.proxynova.com" - headers["Origin"] = "https://www.proxynova.com" - headers["Referer"] = "https://www.proxynova.com/proxy-server-list/country-#{country_code}/" - - response = client.get("/proxy-server-list/country-#{country_code}/", headers) - client.close - document = XML.parse_html(response.body) - - proxies = [] of {ip: String, port: Int32, score: Float64} - document.xpath_nodes(%q(//tr[@data-proxy-id])).each do |node| - ip = node.xpath_node(%q(.//td/abbr/script)).not_nil!.content - ip = ip.match(/document\.write\('(?[^']+)'.substr\(8\) \+ '(?[^']+)'/).not_nil! - ip = "#{ip["sub1"][8..-1]}#{ip["sub2"]}" - port = node.xpath_node(%q(.//td[2])).not_nil!.content.strip.to_i - - anchor = node.xpath_node(%q(.//td[4]/div)).not_nil! - speed = anchor["data-value"].to_f - latency = anchor["title"].to_f - uptime = node.xpath_node(%q(.//td[5]/span)).not_nil!.content.rchop("%").to_f - - # TODO: Tweak me - score = (uptime*4 + speed*2 + latency)/7 - proxies << {ip: ip, port: port, score: score} - end - - # proxies = proxies.sort_by { |proxy| proxy[:score] }.reverse - return proxies -end - -def get_spys_proxies(country_code = "US") - client = HTTP::Client.new(URI.parse("http://spys.one")) - client.read_timeout = 10.seconds - client.connect_timeout = 10.seconds - - headers = HTTP::Headers.new - headers["User-Agent"] = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36" - headers["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8" - headers["Accept-Language"] = "Accept-Language: en-US,en;q=0.9" - headers["Host"] = "spys.one" - headers["Origin"] = "http://spys.one" - headers["Referer"] = "http://spys.one/free-proxy-list/#{country_code}/" - headers["Content-Type"] = "application/x-www-form-urlencoded" - body = { - "xpp" => "5", - "xf1" => "0", - "xf2" => "0", - "xf4" => "0", - "xf5" => "1", - } - - response = client.post("/free-proxy-list/#{country_code}/", headers, form: body) - client.close - 20.times do - if response.status_code == 200 - break - end - response = client.post("/free-proxy-list/#{country_code}/", headers, form: body) - end - - response = XML.parse_html(response.body) - - mapping = response.xpath_node(%q(.//body/script)).not_nil!.content - mapping = mapping.match(/\}\('(?

    [^']+)',\d+,\d+,'(?[^']+)'/).not_nil! - p = mapping["p"].not_nil! - x = mapping["x"].not_nil! - mapping = decrypt_port(p, x) - - proxies = [] of {ip: String, port: Int32, score: Float64} - response = response.xpath_node(%q(//tr/td/table)).not_nil! - response.xpath_nodes(%q(.//tr)).each do |node| - if !node["onmouseover"]? - next - end - - ip = node.xpath_node(%q(.//td[1]/font[2])).to_s.match(/(?

    [^<]+)"\+(?[\d\D]+)\)$/).not_nil!["encrypted_port"] - - port = "" - encrypted_port.split("+").each do |number| - number = number.delete("()") - left_side, right_side = number.split("^") - result = mapping[left_side] ^ mapping[right_side] - port = "#{port}#{result}" - end - port = port.to_i - - latency = node.xpath_node(%q(.//td[6])).not_nil!.content.to_f - speed = node.xpath_node(%q(.//td[7]/font/table)).not_nil!["width"].to_f - uptime = node.xpath_node(%q(.//td[8]/font/acronym)).not_nil! - - # Skip proxies that are down - if uptime["title"].ends_with? "?" - next - end - - if md = uptime.content.match(/^\d+/) - uptime = md[0].to_f - else - next - end - - score = (uptime*4 + speed*2 + latency)/7 - - proxies << {ip: ip, port: port, score: score} - end - - proxies = proxies.sort_by!(&.[:score]).reverse! - return proxies -end - -def decrypt_port(p, x) - x = x.split("^") - s = {} of String => String - - 60.times do |i| - if x[i]?.try &.empty? - s[y_func(i)] = y_func(i) - else - s[y_func(i)] = x[i] - end - end - - x = s - p = p.gsub(/\b\w+\b/, x) - - p = p.split(";") - p = p.map(&.split("=")) - - mapping = {} of String => Int32 - p.each do |item| - if item == [""] - next - end - - key = item[0] - value = item[1] - value = value.split("^") - - if value.size == 1 - value = value[0].to_i - else - left_side = value[0].to_i? - left_side ||= mapping[value[0]] - right_side = value[1].to_i? - right_side ||= mapping[value[1]] - - value = left_side ^ right_side - end - - mapping[key] = value - end - - return mapping -end - -def y_func(c) - return (c < 60 ? "" : y_func((c/60).to_i)) + ((c = c % 60) > 35 ? ((c.to_u8 + 29).unsafe_chr) : c.to_s(36)) -end - -PROXY_LIST = { - "GB" => [{ip: "147.135.206.233", port: 3128}, {ip: "167.114.180.102", port: 8080}, {ip: "176.35.250.108", port: 8080}, {ip: "5.148.128.44", port: 80}, {ip: "62.7.85.234", port: 8080}, {ip: "88.150.135.10", port: 36624}], - "DE" => [{ip: "138.201.223.250", port: 31288}, {ip: "138.68.73.59", port: 32574}, {ip: "159.69.211.173", port: 3128}, {ip: "173.249.43.105", port: 3128}, {ip: "212.202.244.90", port: 8080}, {ip: "5.56.18.35", port: 38827}], - "FR" => [{ip: "137.74.254.242", port: 3128}, {ip: "151.80.143.155", port: 53281}, {ip: "178.33.150.97", port: 3128}, {ip: "37.187.2.31", port: 3128}, {ip: "5.135.164.72", port: 3128}, {ip: "5.39.91.73", port: 3128}, {ip: "51.38.162.2", port: 32231}, {ip: "51.38.217.121", port: 808}, {ip: "51.75.109.81", port: 3128}, {ip: "51.75.109.82", port: 3128}, {ip: "51.75.109.83", port: 3128}, {ip: "51.75.109.84", port: 3128}, {ip: "51.75.109.86", port: 3128}, {ip: "51.75.109.88", port: 3128}, {ip: "51.75.109.90", port: 3128}, {ip: "62.210.167.3", port: 3128}, {ip: "90.63.218.232", port: 8080}, {ip: "91.134.165.198", port: 9999}], - "IN" => [{ip: "1.186.151.206", port: 36253}, {ip: "1.186.63.130", port: 39142}, {ip: "103.105.40.1", port: 16538}, {ip: "103.105.40.153", port: 16538}, {ip: "103.106.148.203", port: 60227}, {ip: "103.106.148.207", port: 51451}, {ip: "103.12.246.12", port: 8080}, {ip: "103.14.235.109", port: 8080}, {ip: "103.14.235.26", port: 8080}, {ip: "103.198.172.4", port: 50820}, {ip: "103.205.112.1", port: 23500}, {ip: "103.209.64.19", port: 6666}, {ip: "103.211.76.5", port: 8080}, {ip: "103.216.82.19", port: 6666}, {ip: "103.216.82.190", port: 6666}, {ip: "103.216.82.209", port: 54806}, {ip: "103.216.82.214", port: 6666}, {ip: "103.216.82.37", port: 6666}, {ip: "103.216.82.44", port: 8080}, {ip: "103.216.82.50", port: 53281}, {ip: "103.22.173.230", port: 8080}, {ip: "103.224.38.2", port: 83}, {ip: "103.226.142.90", port: 41386}, {ip: "103.236.114.38", port: 49638}, {ip: "103.240.161.107", port: 6666}, {ip: "103.240.161.108", port: 6666}, {ip: "103.240.161.109", port: 6666}, {ip: "103.240.161.59", port: 48809}, {ip: "103.245.198.101", port: 8080}, {ip: "103.250.148.82", port: 6666}, {ip: "103.251.58.51", port: 61489}, {ip: "103.253.169.115", port: 32731}, {ip: "103.253.211.182", port: 8080}, {ip: "103.253.211.182", port: 80}, {ip: "103.255.234.169", port: 39847}, {ip: "103.42.161.118", port: 8080}, {ip: "103.42.162.30", port: 8080}, {ip: "103.42.162.50", port: 8080}, {ip: "103.42.162.58", port: 8080}, {ip: "103.46.233.12", port: 83}, {ip: "103.46.233.13", port: 83}, {ip: "103.46.233.16", port: 83}, {ip: "103.46.233.17", port: 83}, {ip: "103.46.233.21", port: 83}, {ip: "103.46.233.23", port: 83}, {ip: "103.46.233.29", port: 81}, {ip: "103.46.233.29", port: 83}, {ip: "103.46.233.50", port: 83}, {ip: "103.47.153.87", port: 8080}, {ip: "103.47.66.2", port: 39804}, {ip: "103.49.53.1", port: 81}, {ip: "103.52.220.1", port: 49068}, {ip: "103.56.228.166", port: 53281}, {ip: "103.56.30.128", port: 8080}, {ip: "103.65.193.17", port: 50862}, {ip: "103.65.195.1", port: 33960}, {ip: "103.69.220.14", port: 3128}, {ip: "103.70.128.84", port: 8080}, {ip: "103.70.128.86", port: 8080}, {ip: "103.70.131.74", port: 8080}, {ip: "103.70.146.250", port: 59563}, {ip: "103.72.216.194", port: 38345}, {ip: "103.75.161.38", port: 21776}, {ip: "103.76.253.155", port: 3128}, {ip: "103.87.104.137", port: 8080}, {ip: "110.235.198.3", port: 57660}, {ip: "114.69.229.161", port: 8080}, {ip: "117.196.231.201", port: 37769}, {ip: "117.211.166.214", port: 3128}, {ip: "117.240.175.51", port: 3128}, {ip: "117.240.210.155", port: 53281}, {ip: "117.240.59.115", port: 36127}, {ip: "117.242.154.73", port: 33889}, {ip: "117.244.15.243", port: 3128}, {ip: "119.235.54.3", port: 8080}, {ip: "120.138.117.102", port: 59308}, {ip: "123.108.200.185", port: 83}, {ip: "123.108.200.217", port: 82}, {ip: "123.176.43.218", port: 40524}, {ip: "125.21.43.82", port: 8080}, {ip: "125.62.192.225", port: 82}, {ip: "125.62.192.33", port: 84}, {ip: "125.62.194.1", port: 83}, {ip: "125.62.213.134", port: 82}, {ip: "125.62.213.18", port: 83}, {ip: "125.62.213.201", port: 84}, {ip: "125.62.213.242", port: 83}, {ip: "125.62.214.185", port: 84}, {ip: "139.5.26.27", port: 53281}, {ip: "14.102.67.101", port: 30337}, {ip: "14.142.122.134", port: 8080}, {ip: "150.129.114.194", port: 6666}, {ip: "150.129.151.62", port: 6666}, {ip: "150.129.171.115", port: 6666}, {ip: "150.129.201.30", port: 6666}, {ip: "157.119.207.38", port: 53281}, {ip: "175.100.185.151", port: 53281}, {ip: "182.18.177.114", port: 56173}, {ip: "182.73.194.170", port: 8080}, {ip: "182.74.85.230", port: 51214}, {ip: "183.82.116.56", port: 8080}, {ip: "183.82.32.56", port: 49551}, {ip: "183.87.14.229", port: 53281}, {ip: "183.87.14.250", port: 44915}, {ip: "202.134.160.168", port: 8080}, {ip: "202.134.166.1", port: 8080}, {ip: "202.134.180.50", port: 8080}, {ip: "202.62.84.210", port: 53281}, {ip: "203.192.193.225", port: 8080}, {ip: "203.192.195.14", port: 31062}, {ip: "203.192.217.11", port: 8080}, {ip: "223.196.83.182", port: 53281}, {ip: "27.116.20.169", port: 36630}, {ip: "27.116.20.209", port: 36630}, {ip: "27.116.51.21", port: 36033}, {ip: "43.224.8.114", port: 50333}, {ip: "43.224.8.116", port: 6666}, {ip: "43.224.8.124", port: 6666}, {ip: "43.224.8.86", port: 6666}, {ip: "43.225.20.73", port: 8080}, {ip: "43.225.23.26", port: 8080}, {ip: "43.230.196.98", port: 36569}, {ip: "43.240.5.225", port: 31777}, {ip: "43.241.28.248", port: 8080}, {ip: "43.242.209.201", port: 8080}, {ip: "43.246.139.82", port: 8080}, {ip: "43.248.73.86", port: 53281}, {ip: "43.251.170.145", port: 54059}, {ip: "45.112.57.230", port: 61222}, {ip: "45.115.171.30", port: 47949}, {ip: "45.121.29.254", port: 54858}, {ip: "45.123.26.146", port: 53281}, {ip: "45.125.61.193", port: 32804}, {ip: "45.125.61.209", port: 32804}, {ip: "45.127.121.194", port: 53281}, {ip: "45.250.226.14", port: 3128}, {ip: "45.250.226.38", port: 8080}, {ip: "45.250.226.47", port: 8080}, {ip: "45.250.226.55", port: 8080}, {ip: "49.249.251.86", port: 53281}], - "CN" => [{ip: "182.61.170.45", port: 3128}], - "RU" => [{ip: "109.106.139.225", port: 45689}, {ip: "109.161.48.228", port: 53281}, {ip: "109.167.224.198", port: 51919}, {ip: "109.172.57.250", port: 23500}, {ip: "109.194.2.126", port: 61822}, {ip: "109.195.150.128", port: 37564}, {ip: "109.201.96.171", port: 31773}, {ip: "109.201.97.204", port: 41258}, {ip: "109.201.97.235", port: 39125}, {ip: "109.206.140.74", port: 45991}, {ip: "109.206.148.31", port: 30797}, {ip: "109.69.75.5", port: 46347}, {ip: "109.71.181.170", port: 53983}, {ip: "109.74.132.190", port: 42663}, {ip: "109.74.143.45", port: 36529}, {ip: "109.75.140.158", port: 59916}, {ip: "109.95.84.114", port: 52125}, {ip: "130.255.12.24", port: 31004}, {ip: "134.19.147.72", port: 44812}, {ip: "134.90.181.7", port: 54353}, {ip: "145.255.6.171", port: 31252}, {ip: "146.120.227.3", port: 8080}, {ip: "149.255.112.194", port: 48968}, {ip: "158.46.127.222", port: 52574}, {ip: "158.46.43.144", port: 39120}, {ip: "158.58.130.185", port: 50016}, {ip: "158.58.132.12", port: 56962}, {ip: "158.58.133.106", port: 41258}, {ip: "158.58.133.13", port: 21213}, {ip: "176.101.0.47", port: 34471}, {ip: "176.101.89.226", port: 33470}, {ip: "176.106.12.65", port: 30120}, {ip: "176.107.80.110", port: 58901}, {ip: "176.110.121.9", port: 46322}, {ip: "176.110.121.90", port: 21776}, {ip: "176.111.97.18", port: 8080}, {ip: "176.112.106.230", port: 33996}, {ip: "176.112.110.40", port: 61142}, {ip: "176.113.116.70", port: 55589}, {ip: "176.113.27.192", port: 47337}, {ip: "176.115.197.118", port: 8080}, {ip: "176.117.255.182", port: 53100}, {ip: "176.120.200.69", port: 44331}, {ip: "176.124.123.93", port: 41258}, {ip: "176.192.124.98", port: 60787}, {ip: "176.192.5.238", port: 61227}, {ip: "176.192.8.206", port: 39422}, {ip: "176.193.15.94", port: 8080}, {ip: "176.196.195.170", port: 48129}, {ip: "176.196.198.154", port: 35252}, {ip: "176.196.238.234", port: 44648}, {ip: "176.196.239.46", port: 35656}, {ip: "176.196.246.6", port: 53281}, {ip: "176.196.84.138", port: 51336}, {ip: "176.197.145.246", port: 32649}, {ip: "176.197.99.142", port: 47278}, {ip: "176.215.1.108", port: 60339}, {ip: "176.215.170.147", port: 35604}, {ip: "176.56.23.14", port: 35340}, {ip: "176.62.185.54", port: 53883}, {ip: "176.74.13.110", port: 8080}, {ip: "178.130.29.226", port: 53295}, {ip: "178.170.254.178", port: 46788}, {ip: "178.213.13.136", port: 53281}, {ip: "178.218.104.8", port: 49707}, {ip: "178.219.183.163", port: 8080}, {ip: "178.237.180.34", port: 57307}, {ip: "178.57.101.212", port: 38020}, {ip: "178.57.101.235", port: 31309}, {ip: "178.64.190.133", port: 46688}, {ip: "178.75.1.111", port: 50411}, {ip: "178.75.27.131", port: 41879}, {ip: "185.13.35.178", port: 40654}, {ip: "185.15.189.67", port: 30215}, {ip: "185.175.119.137", port: 41258}, {ip: "185.18.111.194", port: 41258}, {ip: "185.19.176.237", port: 53281}, {ip: "185.190.40.115", port: 31747}, {ip: "185.216.195.134", port: 61287}, {ip: "185.22.172.94", port: 10010}, {ip: "185.22.172.94", port: 1448}, {ip: "185.22.174.65", port: 10010}, {ip: "185.22.174.65", port: 1448}, {ip: "185.23.64.100", port: 3130}, {ip: "185.23.82.39", port: 59248}, {ip: "185.233.94.105", port: 59288}, {ip: "185.233.94.146", port: 57736}, {ip: "185.3.68.54", port: 53500}, {ip: "185.32.120.177", port: 60724}, {ip: "185.34.20.164", port: 53700}, {ip: "185.34.23.43", port: 63238}, {ip: "185.51.60.141", port: 39935}, {ip: "185.61.92.228", port: 33060}, {ip: "185.61.93.67", port: 49107}, {ip: "185.7.233.66", port: 53504}, {ip: "185.72.225.10", port: 56285}, {ip: "185.75.5.158", port: 60819}, {ip: "185.9.86.186", port: 39345}, {ip: "188.133.136.10", port: 47113}, {ip: "188.168.75.254", port: 56899}, {ip: "188.170.41.6", port: 60332}, {ip: "188.187.189.142", port: 38264}, {ip: "188.234.151.103", port: 8080}, {ip: "188.235.11.88", port: 57143}, {ip: "188.235.137.196", port: 23500}, {ip: "188.244.175.2", port: 8080}, {ip: "188.255.82.136", port: 53281}, {ip: "188.43.4.117", port: 60577}, {ip: "188.68.95.166", port: 41258}, {ip: "188.92.242.180", port: 52048}, {ip: "188.93.242.213", port: 49774}, {ip: "192.162.193.243", port: 36910}, {ip: "192.162.214.11", port: 41258}, {ip: "193.106.170.133", port: 38591}, {ip: "193.232.113.244", port: 40412}, {ip: "193.232.234.130", port: 61932}, {ip: "193.242.177.105", port: 53281}, {ip: "193.242.178.50", port: 52376}, {ip: "193.242.178.90", port: 8080}, {ip: "193.33.101.152", port: 34611}, {ip: "194.114.128.149", port: 61213}, {ip: "194.135.15.146", port: 59328}, {ip: "194.135.216.178", port: 56805}, {ip: "194.135.75.74", port: 41258}, {ip: "194.146.201.67", port: 53281}, {ip: "194.186.18.46", port: 56408}, {ip: "194.186.20.62", port: 21231}, {ip: "194.190.171.214", port: 43960}, {ip: "194.9.27.82", port: 42720}, {ip: "195.133.232.58", port: 41733}, {ip: "195.14.114.116", port: 59530}, {ip: "195.14.114.24", port: 56897}, {ip: "195.158.250.97", port: 41582}, {ip: "195.16.48.142", port: 36083}, {ip: "195.191.183.169", port: 47238}, {ip: "195.206.45.112", port: 53281}, {ip: "195.208.172.70", port: 8080}, {ip: "195.209.141.67", port: 31927}, {ip: "195.209.176.2", port: 8080}, {ip: "195.210.144.166", port: 30088}, {ip: "195.211.160.88", port: 44464}, {ip: "195.218.144.182", port: 31705}, {ip: "195.46.168.147", port: 8080}, {ip: "195.9.188.78", port: 53281}, {ip: "195.9.209.10", port: 35242}, {ip: "195.9.223.246", port: 52098}, {ip: "195.9.237.66", port: 8080}, {ip: "195.9.91.66", port: 33199}, {ip: "195.91.132.20", port: 19600}, {ip: "195.98.183.82", port: 30953}, {ip: "212.104.82.246", port: 36495}, {ip: "212.119.229.18", port: 33852}, {ip: "212.13.97.122", port: 30466}, {ip: "212.19.21.19", port: 53264}, {ip: "212.19.5.157", port: 58442}, {ip: "212.19.8.223", port: 30281}, {ip: "212.19.8.239", port: 55602}, {ip: "212.192.202.207", port: 4550}, {ip: "212.22.80.224", port: 34822}, {ip: "212.26.247.178", port: 38418}, {ip: "212.33.228.161", port: 37971}, {ip: "212.33.243.83", port: 38605}, {ip: "212.34.53.126", port: 44369}, {ip: "212.5.107.81", port: 56481}, {ip: "212.7.230.7", port: 51405}, {ip: "212.77.138.161", port: 41258}, {ip: "213.108.221.201", port: 32800}, {ip: "213.109.7.135", port: 59918}, {ip: "213.128.9.204", port: 35549}, {ip: "213.134.196.12", port: 38723}, {ip: "213.168.37.86", port: 8080}, {ip: "213.187.118.184", port: 53281}, {ip: "213.21.23.98", port: 53281}, {ip: "213.210.67.166", port: 53281}, {ip: "213.234.0.242", port: 56503}, {ip: "213.247.192.131", port: 41258}, {ip: "213.251.226.208", port: 56900}, {ip: "213.33.155.80", port: 44387}, {ip: "213.33.199.194", port: 36411}, {ip: "213.33.224.82", port: 8080}, {ip: "213.59.153.19", port: 53281}, {ip: "217.10.45.103", port: 8080}, {ip: "217.107.197.39", port: 33628}, {ip: "217.116.60.66", port: 21231}, {ip: "217.195.87.58", port: 41258}, {ip: "217.197.239.54", port: 34463}, {ip: "217.74.161.42", port: 34175}, {ip: "217.8.84.76", port: 46378}, {ip: "31.131.67.14", port: 8080}, {ip: "31.132.127.142", port: 35432}, {ip: "31.132.218.252", port: 32423}, {ip: "31.173.17.118", port: 51317}, {ip: "31.193.124.70", port: 53281}, {ip: "31.210.211.147", port: 8080}, {ip: "31.220.183.217", port: 53281}, {ip: "31.29.212.82", port: 35066}, {ip: "31.42.254.24", port: 30912}, {ip: "31.47.189.14", port: 38473}, {ip: "37.113.129.98", port: 41665}, {ip: "37.192.103.164", port: 34835}, {ip: "37.192.194.50", port: 50165}, {ip: "37.192.99.151", port: 51417}, {ip: "37.205.83.91", port: 35888}, {ip: "37.233.85.155", port: 53281}, {ip: "37.235.167.66", port: 53281}, {ip: "37.235.65.2", port: 47816}, {ip: "37.235.67.178", port: 34450}, {ip: "37.9.134.133", port: 41262}, {ip: "46.150.174.90", port: 53281}, {ip: "46.151.156.198", port: 56013}, {ip: "46.16.226.10", port: 8080}, {ip: "46.163.131.55", port: 48306}, {ip: "46.173.191.51", port: 53281}, {ip: "46.174.222.61", port: 34977}, {ip: "46.180.96.79", port: 42319}, {ip: "46.181.151.79", port: 39386}, {ip: "46.21.74.130", port: 8080}, {ip: "46.227.162.98", port: 51558}, {ip: "46.229.187.169", port: 53281}, {ip: "46.229.67.198", port: 47437}, {ip: "46.243.179.221", port: 41598}, {ip: "46.254.217.54", port: 53281}, {ip: "46.32.68.188", port: 39707}, {ip: "46.39.224.112", port: 36765}, {ip: "46.63.162.171", port: 8080}, {ip: "46.73.33.253", port: 8080}, {ip: "5.128.32.12", port: 51959}, {ip: "5.129.155.3", port: 51390}, {ip: "5.129.16.27", port: 48935}, {ip: "5.141.81.65", port: 61853}, {ip: "5.16.15.234", port: 8080}, {ip: "5.167.51.235", port: 8080}, {ip: "5.167.96.238", port: 3128}, {ip: "5.19.165.235", port: 30793}, {ip: "5.35.93.157", port: 31773}, {ip: "5.59.137.90", port: 8888}, {ip: "5.8.207.160", port: 57192}, {ip: "62.122.97.66", port: 59143}, {ip: "62.148.151.253", port: 53570}, {ip: "62.152.85.158", port: 31156}, {ip: "62.165.54.153", port: 55522}, {ip: "62.173.140.14", port: 8080}, {ip: "62.173.155.206", port: 41258}, {ip: "62.182.206.19", port: 37715}, {ip: "62.213.14.166", port: 8080}, {ip: "62.76.123.224", port: 8080}, {ip: "77.221.220.133", port: 44331}, {ip: "77.232.153.248", port: 60950}, {ip: "77.233.10.37", port: 54210}, {ip: "77.244.27.109", port: 47554}, {ip: "77.37.142.203", port: 53281}, {ip: "77.39.29.29", port: 49243}, {ip: "77.75.6.34", port: 8080}, {ip: "77.87.102.7", port: 42601}, {ip: "77.94.121.212", port: 36896}, {ip: "77.94.121.51", port: 45293}, {ip: "78.110.154.177", port: 59888}, {ip: "78.140.201.226", port: 8090}, {ip: "78.153.4.122", port: 9001}, {ip: "78.156.225.170", port: 41258}, {ip: "78.156.243.146", port: 59730}, {ip: "78.29.14.201", port: 39001}, {ip: "78.81.24.112", port: 8080}, {ip: "78.85.36.203", port: 8080}, {ip: "79.104.219.125", port: 3128}, {ip: "79.104.55.134", port: 8080}, {ip: "79.137.181.170", port: 8080}, {ip: "79.173.124.194", port: 47832}, {ip: "79.173.124.207", port: 53281}, {ip: "79.174.186.168", port: 45710}, {ip: "79.175.51.13", port: 54853}, {ip: "79.175.57.77", port: 55477}, {ip: "80.234.107.118", port: 56952}, {ip: "80.237.6.1", port: 34880}, {ip: "80.243.14.182", port: 49320}, {ip: "80.251.48.215", port: 45157}, {ip: "80.254.121.66", port: 41055}, {ip: "80.254.125.236", port: 80}, {ip: "80.72.121.185", port: 52379}, {ip: "80.89.133.210", port: 3128}, {ip: "80.91.17.113", port: 41258}, {ip: "81.162.61.166", port: 40392}, {ip: "81.163.57.121", port: 41258}, {ip: "81.163.57.46", port: 41258}, {ip: "81.163.62.136", port: 41258}, {ip: "81.23.112.98", port: 55269}, {ip: "81.23.118.106", port: 60427}, {ip: "81.23.177.245", port: 8080}, {ip: "81.24.126.166", port: 8080}, {ip: "81.30.216.147", port: 41258}, {ip: "81.95.131.10", port: 44292}, {ip: "82.114.125.22", port: 8080}, {ip: "82.151.208.20", port: 8080}, {ip: "83.221.216.110", port: 47326}, {ip: "83.246.139.24", port: 8080}, {ip: "83.97.108.8", port: 41258}, {ip: "84.22.154.76", port: 8080}, {ip: "84.52.110.36", port: 38674}, {ip: "84.52.74.194", port: 8080}, {ip: "84.52.77.227", port: 41806}, {ip: "84.52.79.166", port: 43548}, {ip: "84.52.84.157", port: 44331}, {ip: "84.52.88.125", port: 32666}, {ip: "85.113.48.148", port: 8080}, {ip: "85.113.49.220", port: 8080}, {ip: "85.12.193.210", port: 58470}, {ip: "85.15.179.5", port: 8080}, {ip: "85.173.244.102", port: 53281}, {ip: "85.174.227.52", port: 59280}, {ip: "85.192.184.133", port: 8080}, {ip: "85.192.184.133", port: 80}, {ip: "85.21.240.193", port: 55820}, {ip: "85.21.63.219", port: 53281}, {ip: "85.235.190.18", port: 42494}, {ip: "85.237.56.193", port: 8080}, {ip: "85.91.119.6", port: 8080}, {ip: "86.102.116.30", port: 8080}, {ip: "86.110.30.146", port: 38109}, {ip: "87.117.3.129", port: 3128}, {ip: "87.225.108.195", port: 8080}, {ip: "87.228.103.111", port: 8080}, {ip: "87.228.103.43", port: 8080}, {ip: "87.229.143.10", port: 48872}, {ip: "87.249.205.103", port: 8080}, {ip: "87.249.21.193", port: 43079}, {ip: "87.255.13.217", port: 8080}, {ip: "88.147.159.167", port: 53281}, {ip: "88.200.225.32", port: 38583}, {ip: "88.204.59.177", port: 32666}, {ip: "88.84.209.69", port: 30819}, {ip: "88.87.72.72", port: 8080}, {ip: "88.87.79.20", port: 8080}, {ip: "88.87.91.163", port: 48513}, {ip: "88.87.93.20", port: 33277}, {ip: "89.109.12.82", port: 47972}, {ip: "89.109.21.43", port: 9090}, {ip: "89.109.239.183", port: 41041}, {ip: "89.109.54.137", port: 36469}, {ip: "89.17.37.218", port: 52957}, {ip: "89.189.130.103", port: 32626}, {ip: "89.189.159.214", port: 42530}, {ip: "89.189.174.121", port: 52636}, {ip: "89.23.18.29", port: 53281}, {ip: "89.249.251.21", port: 3128}, {ip: "89.250.149.114", port: 60981}, {ip: "89.250.17.209", port: 8080}, {ip: "89.250.19.173", port: 8080}, {ip: "90.150.87.172", port: 81}, {ip: "90.154.125.173", port: 33078}, {ip: "90.188.38.81", port: 60585}, {ip: "90.189.151.183", port: 32601}, {ip: "91.103.208.114", port: 57063}, {ip: "91.122.100.222", port: 44331}, {ip: "91.122.207.229", port: 8080}, {ip: "91.144.139.93", port: 3128}, {ip: "91.144.142.19", port: 44617}, {ip: "91.146.16.54", port: 57902}, {ip: "91.190.116.194", port: 38783}, {ip: "91.190.80.100", port: 31659}, {ip: "91.190.85.97", port: 34286}, {ip: "91.203.36.188", port: 8080}, {ip: "91.205.131.102", port: 8080}, {ip: "91.205.146.25", port: 37501}, {ip: "91.210.94.212", port: 52635}, {ip: "91.213.23.110", port: 8080}, {ip: "91.215.22.51", port: 53305}, {ip: "91.217.42.3", port: 8080}, {ip: "91.217.42.4", port: 8080}, {ip: "91.220.135.146", port: 41258}, {ip: "91.222.167.213", port: 38057}, {ip: "91.226.140.71", port: 33199}, {ip: "91.235.7.216", port: 59067}, {ip: "92.124.195.22", port: 3128}, {ip: "92.126.193.180", port: 8080}, {ip: "92.241.110.223", port: 53281}, {ip: "92.252.240.1", port: 53281}, {ip: "92.255.164.187", port: 3128}, {ip: "92.255.195.57", port: 53281}, {ip: "92.255.229.146", port: 55785}, {ip: "92.255.5.2", port: 41012}, {ip: "92.38.32.36", port: 56113}, {ip: "92.39.138.98", port: 31150}, {ip: "92.51.16.155", port: 46202}, {ip: "92.55.59.63", port: 33030}, {ip: "93.170.112.200", port: 47995}, {ip: "93.183.86.185", port: 53281}, {ip: "93.188.45.157", port: 8080}, {ip: "93.81.246.5", port: 53281}, {ip: "93.91.112.247", port: 41258}, {ip: "94.127.217.66", port: 40115}, {ip: "94.154.85.214", port: 8080}, {ip: "94.180.106.94", port: 32767}, {ip: "94.180.249.187", port: 38051}, {ip: "94.230.243.6", port: 8080}, {ip: "94.232.57.231", port: 51064}, {ip: "94.24.244.170", port: 48936}, {ip: "94.242.55.108", port: 10010}, {ip: "94.242.55.108", port: 1448}, {ip: "94.242.57.136", port: 10010}, {ip: "94.242.57.136", port: 1448}, {ip: "94.242.58.108", port: 10010}, {ip: "94.242.58.108", port: 1448}, {ip: "94.242.58.14", port: 10010}, {ip: "94.242.58.14", port: 1448}, {ip: "94.242.58.142", port: 10010}, {ip: "94.242.58.142", port: 1448}, {ip: "94.242.59.245", port: 10010}, {ip: "94.242.59.245", port: 1448}, {ip: "94.247.241.70", port: 53640}, {ip: "94.247.62.165", port: 33176}, {ip: "94.253.13.228", port: 54935}, {ip: "94.253.14.187", port: 55045}, {ip: "94.28.94.154", port: 46966}, {ip: "94.73.217.125", port: 40858}, {ip: "95.140.19.9", port: 8080}, {ip: "95.140.20.94", port: 33994}, {ip: "95.154.137.66", port: 41258}, {ip: "95.154.159.119", port: 44242}, {ip: "95.154.82.254", port: 52484}, {ip: "95.161.157.227", port: 43170}, {ip: "95.161.182.146", port: 33877}, {ip: "95.161.189.26", port: 61522}, {ip: "95.165.163.146", port: 8888}, {ip: "95.165.172.90", port: 60496}, {ip: "95.165.182.18", port: 38950}, {ip: "95.165.203.222", port: 33805}, {ip: "95.165.244.122", port: 58162}, {ip: "95.167.123.54", port: 58664}, {ip: "95.167.241.242", port: 49636}, {ip: "95.171.1.92", port: 35956}, {ip: "95.172.52.230", port: 35989}, {ip: "95.181.35.30", port: 40804}, {ip: "95.181.56.178", port: 39144}, {ip: "95.181.75.228", port: 53281}, {ip: "95.188.74.194", port: 57122}, {ip: "95.189.112.214", port: 35508}, {ip: "95.31.10.247", port: 30711}, {ip: "95.31.197.77", port: 41651}, {ip: "95.31.2.199", port: 33632}, {ip: "95.71.125.50", port: 49882}, {ip: "95.73.62.13", port: 32185}, {ip: "95.79.36.55", port: 44861}, {ip: "95.79.55.196", port: 53281}, {ip: "95.79.99.148", port: 3128}, {ip: "95.80.65.39", port: 43555}, {ip: "95.80.93.44", port: 41258}, {ip: "95.80.98.41", port: 8080}, {ip: "95.83.156.250", port: 58438}, {ip: "95.84.128.25", port: 33765}, {ip: "95.84.154.73", port: 57423}], - "CA" => [{ip: "144.217.161.149", port: 8080}, {ip: "24.37.9.6", port: 54154}, {ip: "54.39.138.144", port: 3128}, {ip: "54.39.138.145", port: 3128}, {ip: "54.39.138.151", port: 3128}, {ip: "54.39.138.152", port: 3128}, {ip: "54.39.138.153", port: 3128}, {ip: "54.39.138.154", port: 3128}, {ip: "54.39.138.155", port: 3128}, {ip: "54.39.138.156", port: 3128}, {ip: "54.39.138.157", port: 3128}, {ip: "54.39.53.104", port: 3128}, {ip: "66.70.167.113", port: 3128}, {ip: "66.70.167.116", port: 3128}, {ip: "66.70.167.117", port: 3128}, {ip: "66.70.167.119", port: 3128}, {ip: "66.70.167.120", port: 3128}, {ip: "66.70.167.125", port: 3128}, {ip: "66.70.188.148", port: 3128}, {ip: "70.35.213.229", port: 36127}, {ip: "70.65.233.174", port: 8080}, {ip: "72.139.24.66", port: 38861}, {ip: "74.15.191.160", port: 41564}], - "JP" => [{ip: "47.91.20.67", port: 8080}, {ip: "61.118.35.94", port: 55725}], - "IT" => [{ip: "109.70.201.97", port: 53517}, {ip: "176.31.82.212", port: 8080}, {ip: "185.132.228.118", port: 55583}, {ip: "185.49.58.88", port: 56006}, {ip: "185.94.89.179", port: 41258}, {ip: "213.203.134.10", port: 41258}, {ip: "217.61.172.12", port: 41369}, {ip: "46.232.143.126", port: 41258}, {ip: "46.232.143.253", port: 41258}, {ip: "93.67.154.125", port: 8080}, {ip: "93.67.154.125", port: 80}, {ip: "95.169.95.242", port: 53803}], - "TH" => [{ip: "1.10.184.166", port: 57330}, {ip: "1.10.186.100", port: 55011}, {ip: "1.10.186.209", port: 32431}, {ip: "1.10.186.245", port: 34360}, {ip: "1.10.186.93", port: 53711}, {ip: "1.10.187.118", port: 62000}, {ip: "1.10.187.34", port: 51635}, {ip: "1.10.187.43", port: 38715}, {ip: "1.10.188.181", port: 51093}, {ip: "1.10.188.83", port: 31940}, {ip: "1.10.188.95", port: 30593}, {ip: "1.10.189.58", port: 48564}, {ip: "1.179.157.237", port: 46178}, {ip: "1.179.164.213", port: 8080}, {ip: "1.179.198.37", port: 8080}, {ip: "1.20.100.99", port: 53794}, {ip: "1.20.101.221", port: 55707}, {ip: "1.20.101.254", port: 35394}, {ip: "1.20.101.80", port: 36234}, {ip: "1.20.102.133", port: 40296}, {ip: "1.20.103.13", port: 40544}, {ip: "1.20.103.56", port: 55422}, {ip: "1.20.96.234", port: 53142}, {ip: "1.20.97.54", port: 60122}, {ip: "1.20.99.63", port: 32123}, {ip: "101.108.92.20", port: 8080}, {ip: "101.109.143.71", port: 36127}, {ip: "101.51.141.110", port: 42860}, {ip: "101.51.141.60", port: 60417}, {ip: "103.246.17.237", port: 3128}, {ip: "110.164.73.131", port: 8080}, {ip: "110.164.87.80", port: 35844}, {ip: "110.77.134.106", port: 8080}, {ip: "113.53.29.92", port: 47297}, {ip: "113.53.83.192", port: 32780}, {ip: "113.53.83.195", port: 35686}, {ip: "113.53.91.214", port: 8080}, {ip: "115.87.27.0", port: 53276}, {ip: "118.172.211.3", port: 58535}, {ip: "118.172.211.40", port: 30430}, {ip: "118.174.196.174", port: 23500}, {ip: "118.174.196.203", port: 23500}, {ip: "118.174.220.107", port: 41222}, {ip: "118.174.220.110", port: 39025}, {ip: "118.174.220.115", port: 41011}, {ip: "118.174.220.118", port: 59556}, {ip: "118.174.220.136", port: 55041}, {ip: "118.174.220.163", port: 31561}, {ip: "118.174.220.168", port: 47455}, {ip: "118.174.220.231", port: 40924}, {ip: "118.174.220.238", port: 46326}, {ip: "118.174.234.13", port: 53084}, {ip: "118.174.234.26", port: 41926}, {ip: "118.174.234.32", port: 57403}, {ip: "118.174.234.59", port: 59149}, {ip: "118.174.234.68", port: 42626}, {ip: "118.174.234.83", port: 38006}, {ip: "118.175.207.104", port: 38959}, {ip: "118.175.244.111", port: 8080}, {ip: "118.175.93.207", port: 50738}, {ip: "122.154.38.53", port: 8080}, {ip: "122.154.59.6", port: 8080}, {ip: "122.154.72.102", port: 8080}, {ip: "122.155.222.98", port: 3128}, {ip: "124.121.22.121", port: 61699}, {ip: "125.24.156.16", port: 44321}, {ip: "125.25.165.105", port: 33850}, {ip: "125.25.165.111", port: 40808}, {ip: "125.25.165.42", port: 47221}, {ip: "125.25.201.14", port: 30100}, {ip: "125.26.99.135", port: 55637}, {ip: "125.26.99.141", port: 38537}, {ip: "125.26.99.148", port: 31818}, {ip: "134.236.247.137", port: 8080}, {ip: "159.192.98.224", port: 3128}, {ip: "171.100.2.154", port: 8080}, {ip: "171.100.9.126", port: 49163}, {ip: "180.180.156.116", port: 48431}, {ip: "180.180.156.46", port: 48507}, {ip: "180.180.156.87", port: 36628}, {ip: "180.180.218.204", port: 51565}, {ip: "180.180.8.34", port: 8080}, {ip: "182.52.238.125", port: 58861}, {ip: "182.52.74.73", port: 36286}, {ip: "182.52.74.76", port: 34084}, {ip: "182.52.74.77", port: 34825}, {ip: "182.52.74.78", port: 48708}, {ip: "182.52.90.45", port: 53799}, {ip: "182.53.206.155", port: 34307}, {ip: "182.53.206.43", port: 45330}, {ip: "182.53.206.49", port: 54228}, {ip: "183.88.212.141", port: 8080}, {ip: "183.88.212.184", port: 8080}, {ip: "183.88.213.85", port: 8080}, {ip: "183.88.214.47", port: 8080}, {ip: "184.82.128.211", port: 8080}, {ip: "202.183.201.13", port: 8081}, {ip: "202.29.20.151", port: 43083}, {ip: "203.150.172.151", port: 8080}, {ip: "27.131.157.94", port: 8080}, {ip: "27.145.100.22", port: 8080}, {ip: "27.145.100.243", port: 8080}, {ip: "49.231.196.114", port: 53281}, {ip: "58.97.72.83", port: 8080}, {ip: "61.19.145.66", port: 8080}], - "ES" => [{ip: "185.198.184.14", port: 48122}, {ip: "185.26.226.241", port: 36012}, {ip: "194.224.188.82", port: 3128}, {ip: "195.235.68.61", port: 3128}, {ip: "195.53.237.122", port: 3128}, {ip: "195.53.86.82", port: 3128}, {ip: "213.96.245.47", port: 8080}, {ip: "217.125.71.214", port: 33950}, {ip: "62.14.178.72", port: 53281}, {ip: "80.35.254.42", port: 53281}, {ip: "81.33.4.214", port: 61711}, {ip: "83.175.238.170", port: 53281}, {ip: "85.217.137.77", port: 3128}, {ip: "90.170.205.178", port: 33680}, {ip: "93.156.177.91", port: 53281}, {ip: "95.60.152.139", port: 37995}], - "AE" => [{ip: "178.32.5.90", port: 36159}], - "KR" => [{ip: "112.217.219.179", port: 3128}, {ip: "114.141.229.2", port: 58115}, {ip: "121.139.218.165", port: 31409}, {ip: "122.49.112.2", port: 38592}, {ip: "61.42.18.132", port: 53281}], - "BR" => [{ip: "128.201.97.157", port: 53281}, {ip: "128.201.97.158", port: 53281}, {ip: "131.0.246.157", port: 35252}, {ip: "131.161.26.90", port: 8080}, {ip: "131.72.143.100", port: 41396}, {ip: "138.0.24.66", port: 53281}, {ip: "138.121.130.50", port: 50600}, {ip: "138.121.155.127", port: 61932}, {ip: "138.121.32.133", port: 23492}, {ip: "138.185.176.63", port: 53281}, {ip: "138.204.233.190", port: 53281}, {ip: "138.204.233.242", port: 53281}, {ip: "138.219.71.74", port: 52688}, {ip: "138.36.107.24", port: 41184}, {ip: "138.94.115.166", port: 8080}, {ip: "143.0.188.161", port: 53281}, {ip: "143.202.218.135", port: 8080}, {ip: "143.208.2.42", port: 53281}, {ip: "143.208.79.223", port: 8080}, {ip: "143.255.52.102", port: 40687}, {ip: "143.255.52.116", port: 57856}, {ip: "143.255.52.117", port: 37279}, {ip: "144.217.22.128", port: 8080}, {ip: "168.0.8.225", port: 8080}, {ip: "168.0.8.55", port: 8080}, {ip: "168.121.139.54", port: 40056}, {ip: "168.181.168.23", port: 53281}, {ip: "168.181.170.198", port: 31935}, {ip: "168.232.198.25", port: 32009}, {ip: "168.232.198.35", port: 42267}, {ip: "168.232.207.145", port: 46342}, {ip: "170.0.104.107", port: 60337}, {ip: "170.0.112.2", port: 50359}, {ip: "170.0.112.229", port: 50359}, {ip: "170.238.118.107", port: 34314}, {ip: "170.239.144.9", port: 3128}, {ip: "170.247.29.138", port: 8080}, {ip: "170.81.237.36", port: 37124}, {ip: "170.84.51.74", port: 53281}, {ip: "170.84.60.222", port: 42981}, {ip: "177.10.202.67", port: 8080}, {ip: "177.101.60.86", port: 80}, {ip: "177.103.231.211", port: 55091}, {ip: "177.12.80.50", port: 50556}, {ip: "177.131.13.9", port: 20183}, {ip: "177.135.178.115", port: 42510}, {ip: "177.135.248.75", port: 20183}, {ip: "177.184.206.238", port: 39508}, {ip: "177.185.148.46", port: 58623}, {ip: "177.200.83.238", port: 8080}, {ip: "177.21.24.146", port: 666}, {ip: "177.220.188.120", port: 47556}, {ip: "177.220.188.213", port: 8080}, {ip: "177.222.229.243", port: 23500}, {ip: "177.234.161.42", port: 8080}, {ip: "177.36.11.241", port: 3128}, {ip: "177.36.12.193", port: 23500}, {ip: "177.37.199.175", port: 49608}, {ip: "177.39.187.70", port: 37315}, {ip: "177.44.175.199", port: 8080}, {ip: "177.46.148.126", port: 3128}, {ip: "177.46.148.142", port: 3128}, {ip: "177.47.194.98", port: 21231}, {ip: "177.5.98.58", port: 20183}, {ip: "177.52.55.19", port: 60901}, {ip: "177.54.200.66", port: 57526}, {ip: "177.55.255.74", port: 37147}, {ip: "177.67.217.94", port: 53281}, {ip: "177.73.248.6", port: 54381}, {ip: "177.73.4.234", port: 23500}, {ip: "177.75.143.211", port: 35955}, {ip: "177.75.161.206", port: 3128}, {ip: "177.75.86.49", port: 20183}, {ip: "177.8.216.106", port: 8080}, {ip: "177.8.216.114", port: 8080}, {ip: "177.8.37.247", port: 56052}, {ip: "177.84.216.17", port: 50569}, {ip: "177.85.200.254", port: 53095}, {ip: "177.87.169.1", port: 53281}, {ip: "179.107.97.178", port: 3128}, {ip: "179.109.144.25", port: 8080}, {ip: "179.109.193.137", port: 53281}, {ip: "179.189.125.206", port: 8080}, {ip: "179.97.30.46", port: 53100}, {ip: "186.192.195.220", port: 38983}, {ip: "186.193.11.226", port: 48999}, {ip: "186.193.26.106", port: 3128}, {ip: "186.208.220.248", port: 3128}, {ip: "186.209.243.142", port: 3128}, {ip: "186.209.243.233", port: 3128}, {ip: "186.211.106.227", port: 34334}, {ip: "186.211.160.178", port: 36756}, {ip: "186.215.133.170", port: 20183}, {ip: "186.216.81.21", port: 31773}, {ip: "186.219.214.13", port: 32708}, {ip: "186.224.94.6", port: 48957}, {ip: "186.225.97.246", port: 43082}, {ip: "186.226.171.163", port: 48698}, {ip: "186.226.179.2", port: 56089}, {ip: "186.226.234.67", port: 33834}, {ip: "186.228.147.58", port: 20183}, {ip: "186.233.97.163", port: 8888}, {ip: "186.248.170.82", port: 53281}, {ip: "186.249.213.101", port: 53482}, {ip: "186.249.213.65", port: 52018}, {ip: "186.250.213.225", port: 60774}, {ip: "186.250.96.70", port: 8080}, {ip: "186.250.96.77", port: 8080}, {ip: "187.1.43.246", port: 53396}, {ip: "187.108.36.250", port: 20183}, {ip: "187.108.38.10", port: 20183}, {ip: "187.109.36.251", port: 20183}, {ip: "187.109.40.9", port: 20183}, {ip: "187.109.56.101", port: 20183}, {ip: "187.111.90.89", port: 53281}, {ip: "187.115.10.50", port: 20183}, {ip: "187.19.62.7", port: 59010}, {ip: "187.33.79.61", port: 33469}, {ip: "187.35.158.150", port: 38872}, {ip: "187.44.1.167", port: 8080}, {ip: "187.45.127.87", port: 20183}, {ip: "187.45.156.109", port: 8080}, {ip: "187.5.218.215", port: 20183}, {ip: "187.58.65.225", port: 3128}, {ip: "187.63.111.37", port: 3128}, {ip: "187.72.166.10", port: 8080}, {ip: "187.73.68.14", port: 53281}, {ip: "187.84.177.6", port: 45903}, {ip: "187.84.191.170", port: 43936}, {ip: "187.87.204.210", port: 45597}, {ip: "187.87.39.247", port: 31793}, {ip: "189.1.16.162", port: 23500}, {ip: "189.113.124.162", port: 8080}, {ip: "189.124.195.185", port: 37318}, {ip: "189.3.196.18", port: 61595}, {ip: "189.37.33.59", port: 35532}, {ip: "189.7.49.66", port: 42700}, {ip: "189.90.194.35", port: 30843}, {ip: "189.90.248.75", port: 8080}, {ip: "189.91.231.43", port: 3128}, {ip: "191.239.243.156", port: 3128}, {ip: "191.240.154.246", port: 23500}, {ip: "191.240.156.154", port: 36127}, {ip: "191.240.99.142", port: 9090}, {ip: "191.241.226.230", port: 53281}, {ip: "191.241.228.74", port: 20183}, {ip: "191.241.228.78", port: 20183}, {ip: "191.241.33.238", port: 39188}, {ip: "191.241.36.170", port: 8080}, {ip: "191.241.36.218", port: 3128}, {ip: "191.242.182.132", port: 8081}, {ip: "191.243.221.130", port: 3128}, {ip: "191.255.207.231", port: 20183}, {ip: "191.36.192.196", port: 3128}, {ip: "191.36.244.230", port: 51377}, {ip: "191.5.0.79", port: 53281}, {ip: "191.6.228.6", port: 53281}, {ip: "191.7.193.18", port: 38133}, {ip: "191.7.20.134", port: 3128}, {ip: "192.140.91.173", port: 20183}, {ip: "200.150.86.138", port: 44677}, {ip: "200.155.36.185", port: 3128}, {ip: "200.155.36.188", port: 3128}, {ip: "200.155.39.41", port: 3128}, {ip: "200.174.158.26", port: 34112}, {ip: "200.187.177.105", port: 20183}, {ip: "200.187.87.138", port: 20183}, {ip: "200.192.252.201", port: 8080}, {ip: "200.192.255.102", port: 8080}, {ip: "200.203.144.2", port: 50262}, {ip: "200.229.238.42", port: 20183}, {ip: "200.233.134.85", port: 43172}, {ip: "200.233.136.177", port: 20183}, {ip: "200.241.44.3", port: 20183}, {ip: "200.255.122.170", port: 8080}, {ip: "200.255.122.174", port: 8080}, {ip: "201.12.21.57", port: 8080}, {ip: "201.131.224.21", port: 56200}, {ip: "201.182.223.16", port: 37492}, {ip: "201.20.89.126", port: 8080}, {ip: "201.22.95.10", port: 8080}, {ip: "201.57.167.34", port: 8080}, {ip: "201.59.200.246", port: 80}, {ip: "201.6.167.178", port: 3128}, {ip: "201.90.36.194", port: 3128}, {ip: "45.226.20.6", port: 8080}, {ip: "45.234.139.129", port: 20183}, {ip: "45.234.200.18", port: 53281}, {ip: "45.235.87.4", port: 51996}, {ip: "45.6.136.38", port: 53281}, {ip: "45.6.80.131", port: 52080}, {ip: "45.6.93.10", port: 8080}, {ip: "45.71.108.162", port: 53281}], - "PK" => [{ip: "103.18.243.154", port: 8080}, {ip: "110.36.218.126", port: 36651}, {ip: "110.36.234.210", port: 8080}, {ip: "110.39.162.74", port: 53281}, {ip: "110.39.174.58", port: 8080}, {ip: "111.68.108.34", port: 8080}, {ip: "125.209.116.182", port: 31653}, {ip: "125.209.78.21", port: 8080}, {ip: "125.209.82.78", port: 35087}, {ip: "180.92.156.150", port: 8080}, {ip: "202.142.158.114", port: 8080}, {ip: "202.147.173.10", port: 8080}, {ip: "202.147.173.10", port: 80}, {ip: "202.69.38.82", port: 8080}, {ip: "203.128.16.126", port: 59538}, {ip: "203.128.16.154", port: 33002}, {ip: "27.255.4.170", port: 8080}], - "ID" => [{ip: "101.128.68.113", port: 8080}, {ip: "101.255.116.113", port: 53281}, {ip: "101.255.120.170", port: 6969}, {ip: "101.255.121.74", port: 8080}, {ip: "101.255.124.242", port: 8080}, {ip: "101.255.124.242", port: 80}, {ip: "101.255.56.138", port: 53560}, {ip: "103.10.171.132", port: 41043}, {ip: "103.10.81.172", port: 80}, {ip: "103.108.158.3", port: 48196}, {ip: "103.111.219.159", port: 53281}, {ip: "103.111.54.26", port: 49781}, {ip: "103.111.54.74", port: 8080}, {ip: "103.19.110.177", port: 8080}, {ip: "103.2.146.66", port: 49089}, {ip: "103.206.168.177", port: 53281}, {ip: "103.206.253.58", port: 49573}, {ip: "103.21.92.254", port: 33929}, {ip: "103.226.49.83", port: 23500}, {ip: "103.227.147.142", port: 37581}, {ip: "103.23.101.58", port: 8080}, {ip: "103.24.107.2", port: 8181}, {ip: "103.245.19.222", port: 53281}, {ip: "103.247.122.38", port: 8080}, {ip: "103.247.218.166", port: 3128}, {ip: "103.248.219.26", port: 53634}, {ip: "103.253.2.165", port: 33543}, {ip: "103.253.2.168", port: 51229}, {ip: "103.253.2.174", port: 30827}, {ip: "103.28.114.134", port: 8080}, {ip: "103.28.220.73", port: 53281}, {ip: "103.30.246.47", port: 3128}, {ip: "103.31.45.169", port: 57655}, {ip: "103.41.122.14", port: 53281}, {ip: "103.75.101.97", port: 8080}, {ip: "103.76.17.151", port: 23500}, {ip: "103.76.50.181", port: 8080}, {ip: "103.76.50.181", port: 80}, {ip: "103.76.50.182", port: 8080}, {ip: "103.78.74.170", port: 3128}, {ip: "103.78.80.194", port: 33442}, {ip: "103.8.122.5", port: 53297}, {ip: "103.80.236.107", port: 53281}, {ip: "103.80.238.203", port: 53281}, {ip: "103.86.140.74", port: 59538}, {ip: "103.94.122.254", port: 8080}, {ip: "103.94.125.244", port: 41508}, {ip: "103.94.169.19", port: 8080}, {ip: "103.94.7.254", port: 53281}, {ip: "106.0.51.50", port: 17385}, {ip: "110.93.13.202", port: 34881}, {ip: "112.78.37.6", port: 54791}, {ip: "114.199.110.58", port: 55898}, {ip: "114.199.112.170", port: 23500}, {ip: "114.199.123.194", port: 8080}, {ip: "114.57.33.162", port: 46935}, {ip: "114.57.33.214", port: 8080}, {ip: "114.6.197.254", port: 8080}, {ip: "114.7.15.146", port: 8080}, {ip: "114.7.162.254", port: 53281}, {ip: "115.124.75.226", port: 53990}, {ip: "115.124.75.228", port: 3128}, {ip: "117.102.78.42", port: 8080}, {ip: "117.102.93.251", port: 8080}, {ip: "117.102.94.186", port: 8080}, {ip: "117.102.94.186", port: 80}, {ip: "117.103.2.249", port: 58276}, {ip: "117.54.13.174", port: 34190}, {ip: "117.74.124.129", port: 8088}, {ip: "118.97.100.83", port: 35220}, {ip: "118.97.191.162", port: 80}, {ip: "118.97.191.203", port: 8080}, {ip: "118.97.36.18", port: 8080}, {ip: "118.97.73.85", port: 53281}, {ip: "118.99.105.226", port: 8080}, {ip: "119.252.168.53", port: 53281}, {ip: "122.248.45.35", port: 53281}, {ip: "122.50.6.186", port: 8080}, {ip: "122.50.6.186", port: 80}, {ip: "123.231.226.114", port: 47562}, {ip: "123.255.202.83", port: 32523}, {ip: "124.158.164.195", port: 8080}, {ip: "124.81.99.30", port: 3128}, {ip: "137.59.162.10", port: 3128}, {ip: "139.0.29.20", port: 59532}, {ip: "139.255.123.194", port: 4550}, {ip: "139.255.16.171", port: 31773}, {ip: "139.255.17.2", port: 47421}, {ip: "139.255.19.162", port: 42371}, {ip: "139.255.7.81", port: 53281}, {ip: "139.255.91.115", port: 8080}, {ip: "139.255.92.26", port: 53281}, {ip: "158.140.181.140", port: 54041}, {ip: "160.202.40.20", port: 55655}, {ip: "175.103.42.147", port: 8080}, {ip: "180.178.98.198", port: 8080}, {ip: "180.250.101.146", port: 8080}, {ip: "182.23.107.212", port: 3128}, {ip: "182.23.2.101", port: 49833}, {ip: "182.23.7.226", port: 8080}, {ip: "182.253.209.203", port: 3128}, {ip: "183.91.66.210", port: 80}, {ip: "202.137.10.179", port: 57338}, {ip: "202.137.25.53", port: 3128}, {ip: "202.137.25.8", port: 8080}, {ip: "202.138.242.76", port: 4550}, {ip: "202.138.249.202", port: 43108}, {ip: "202.148.2.254", port: 8000}, {ip: "202.162.201.94", port: 53281}, {ip: "202.165.47.26", port: 8080}, {ip: "202.43.167.130", port: 8080}, {ip: "202.51.126.10", port: 53281}, {ip: "202.59.171.164", port: 58567}, {ip: "202.93.128.98", port: 3128}, {ip: "203.142.72.114", port: 808}, {ip: "203.153.117.65", port: 54144}, {ip: "203.189.89.1", port: 53281}, {ip: "203.77.239.18", port: 37002}, {ip: "203.99.123.25", port: 61502}, {ip: "220.247.168.163", port: 53281}, {ip: "220.247.173.154", port: 53281}, {ip: "220.247.174.206", port: 53445}, {ip: "222.124.131.211", port: 47343}, {ip: "222.124.173.146", port: 53281}, {ip: "222.124.2.131", port: 8080}, {ip: "222.124.2.186", port: 8080}, {ip: "222.124.215.187", port: 38913}, {ip: "222.124.221.179", port: 53281}, {ip: "223.25.101.242", port: 59504}, {ip: "223.25.97.62", port: 8080}, {ip: "223.25.99.38", port: 80}, {ip: "27.111.44.202", port: 80}, {ip: "27.111.47.3", port: 51144}, {ip: "36.37.124.234", port: 36179}, {ip: "36.37.124.235", port: 36179}, {ip: "36.37.81.135", port: 8080}, {ip: "36.37.89.98", port: 32323}, {ip: "36.66.217.179", port: 8080}, {ip: "36.66.98.6", port: 53281}, {ip: "36.67.143.183", port: 48746}, {ip: "36.67.206.187", port: 8080}, {ip: "36.67.32.87", port: 8080}, {ip: "36.67.93.220", port: 3128}, {ip: "36.67.93.220", port: 80}, {ip: "36.89.10.51", port: 34115}, {ip: "36.89.119.149", port: 8080}, {ip: "36.89.157.23", port: 37728}, {ip: "36.89.181.155", port: 60165}, {ip: "36.89.188.11", port: 39507}, {ip: "36.89.194.113", port: 37811}, {ip: "36.89.226.254", port: 8081}, {ip: "36.89.232.138", port: 23500}, {ip: "36.89.39.10", port: 3128}, {ip: "36.89.65.253", port: 60997}, {ip: "43.243.141.114", port: 8080}, {ip: "43.245.184.202", port: 41102}, {ip: "43.245.184.238", port: 80}, {ip: "66.96.233.225", port: 35053}, {ip: "66.96.237.253", port: 8080}], - "BD" => [{ip: "103.103.88.91", port: 8080}, {ip: "103.106.119.154", port: 8080}, {ip: "103.106.236.1", port: 8080}, {ip: "103.106.236.41", port: 8080}, {ip: "103.108.144.139", port: 53281}, {ip: "103.109.57.218", port: 8080}, {ip: "103.109.58.242", port: 8080}, {ip: "103.112.129.106", port: 31094}, {ip: "103.112.129.82", port: 53281}, {ip: "103.114.10.177", port: 8080}, {ip: "103.114.10.250", port: 8080}, {ip: "103.15.245.26", port: 8080}, {ip: "103.195.204.73", port: 21776}, {ip: "103.197.49.106", port: 49688}, {ip: "103.198.168.29", port: 21776}, {ip: "103.214.200.6", port: 59008}, {ip: "103.218.25.161", port: 8080}, {ip: "103.218.25.41", port: 8080}, {ip: "103.218.26.204", port: 8080}, {ip: "103.218.27.221", port: 8080}, {ip: "103.231.229.90", port: 53281}, {ip: "103.239.252.233", port: 8080}, {ip: "103.239.252.50", port: 8080}, {ip: "103.239.253.193", port: 8080}, {ip: "103.250.68.193", port: 51370}, {ip: "103.5.232.146", port: 8080}, {ip: "103.73.224.53", port: 23500}, {ip: "103.9.134.73", port: 65301}, {ip: "113.11.47.242", port: 40071}, {ip: "113.11.5.67", port: 40071}, {ip: "114.31.5.34", port: 52606}, {ip: "115.127.51.226", port: 42764}, {ip: "115.127.64.62", port: 39611}, {ip: "115.127.91.106", port: 8080}, {ip: "119.40.85.198", port: 36899}, {ip: "123.200.29.110", port: 23500}, {ip: "123.49.51.42", port: 55124}, {ip: "163.47.36.90", port: 3128}, {ip: "180.211.134.158", port: 23500}, {ip: "180.211.193.74", port: 40536}, {ip: "180.92.238.226", port: 53451}, {ip: "182.160.104.213", port: 8080}, {ip: "202.191.126.58", port: 23500}, {ip: "202.4.126.170", port: 8080}, {ip: "202.5.37.241", port: 33623}, {ip: "202.5.57.5", port: 61729}, {ip: "202.79.17.65", port: 60122}, {ip: "203.188.248.52", port: 23500}, {ip: "27.147.146.78", port: 52220}, {ip: "27.147.164.10", port: 52344}, {ip: "27.147.212.38", port: 53281}, {ip: "27.147.217.154", port: 43252}, {ip: "27.147.219.102", port: 49464}, {ip: "43.239.74.137", port: 8080}, {ip: "43.240.103.252", port: 8080}, {ip: "45.125.223.57", port: 8080}, {ip: "45.125.223.81", port: 8080}, {ip: "45.251.228.122", port: 41418}, {ip: "45.64.132.137", port: 8080}, {ip: "45.64.132.137", port: 80}, {ip: "61.247.186.137", port: 8080}], - "MX" => [{ip: "148.217.94.54", port: 3128}, {ip: "177.244.28.77", port: 53281}, {ip: "187.141.73.147", port: 53281}, {ip: "187.185.15.35", port: 53281}, {ip: "187.188.46.172", port: 53455}, {ip: "187.216.83.185", port: 8080}, {ip: "187.216.90.46", port: 53281}, {ip: "187.243.253.182", port: 33796}, {ip: "189.195.132.86", port: 43286}, {ip: "189.204.158.161", port: 8080}, {ip: "200.79.180.115", port: 8080}, {ip: "201.140.113.90", port: 37193}, {ip: "201.144.14.229", port: 53281}, {ip: "201.163.73.93", port: 53281}], - "PH" => [{ip: "103.86.187.242", port: 23500}, {ip: "122.54.101.69", port: 8080}, {ip: "122.54.65.150", port: 8080}, {ip: "125.5.20.134", port: 53281}, {ip: "146.88.77.51", port: 8080}, {ip: "182.18.200.92", port: 8080}, {ip: "219.90.87.91", port: 53281}, {ip: "58.69.12.210", port: 8080}], - "EG" => [{ip: "41.65.0.167", port: 8080}], - "VN" => [{ip: "1.55.240.156", port: 53281}, {ip: "101.99.23.136", port: 3128}, {ip: "103.15.51.160", port: 8080}, {ip: "113.161.128.169", port: 60427}, {ip: "113.161.161.143", port: 57967}, {ip: "113.161.173.10", port: 3128}, {ip: "113.161.35.108", port: 30028}, {ip: "113.164.79.177", port: 46281}, {ip: "113.190.235.50", port: 34619}, {ip: "115.78.160.247", port: 8080}, {ip: "117.2.155.29", port: 47228}, {ip: "117.2.17.26", port: 53281}, {ip: "117.2.22.41", port: 41973}, {ip: "117.4.145.16", port: 51487}, {ip: "118.69.219.185", port: 55184}, {ip: "118.69.61.212", port: 53281}, {ip: "118.70.116.227", port: 61651}, {ip: "118.70.219.124", port: 53281}, {ip: "221.121.12.238", port: 36077}, {ip: "27.2.7.59", port: 52148}], - "CD" => [{ip: "41.79.233.45", port: 8080}], - "TR" => [{ip: "151.80.65.175", port: 3128}, {ip: "176.235.186.242", port: 37043}, {ip: "178.250.92.18", port: 8080}, {ip: "185.203.170.92", port: 8080}, {ip: "185.203.170.94", port: 8080}, {ip: "185.203.170.95", port: 8080}, {ip: "185.51.36.152", port: 41258}, {ip: "195.137.223.50", port: 41336}, {ip: "195.155.98.70", port: 52598}, {ip: "212.156.146.22", port: 40080}, {ip: "213.14.31.122", port: 44621}, {ip: "31.145.137.139", port: 31871}, {ip: "31.145.138.129", port: 31871}, {ip: "31.145.138.146", port: 34159}, {ip: "31.145.187.172", port: 30636}, {ip: "78.188.4.124", port: 34514}, {ip: "88.248.23.216", port: 36426}, {ip: "93.182.72.36", port: 8080}, {ip: "95.0.194.241", port: 9090}], -} diff --git a/src/invidious/yt_backend/youtube_api.cr b/src/invidious/yt_backend/youtube_api.cr index 9e0631f6..8235898f 100644 --- a/src/invidious/yt_backend/youtube_api.cr +++ b/src/invidious/yt_backend/youtube_api.cr @@ -188,10 +188,6 @@ module YoutubeAPI # conf_2 = ClientConfig.new(client_type: ClientType::Android) # YoutubeAPI::player(video_id: "dQw4w9WgXcQ", client_config: conf_2) # - # # Proxy request through russian proxies - # conf_3 = ClientConfig.new(proxy_region: "RU") - # YoutubeAPI::next({video_id: "dQw4w9WgXcQ"}, client_config: conf_3) - # ``` # struct ClientConfig # Type of client to emulate. @@ -202,16 +198,11 @@ module YoutubeAPI # (this is passed as the `gl` parameter). property region : String | Nil - # ISO code of country where the proxy is located. - # Used in case of geo-restricted videos. - property proxy_region : String | Nil - # Initialization function def initialize( *, @client_type = ClientType::Web, - @region = "US", - @proxy_region = nil + @region = "US" ) end @@ -271,9 +262,8 @@ module YoutubeAPI # Convert to string, for logging purposes def to_s return { - client_type: self.name, - region: @region, - proxy_region: @proxy_region, + client_type: self.name, + region: @region, }.to_s end end @@ -620,7 +610,7 @@ module YoutubeAPI LOGGER.trace("YoutubeAPI: POST data: #{data}") # Send the POST request - body = YT_POOL.client(client_config.proxy_region) do |client| + body = YT_POOL.client() do |client| client.post(url, headers: headers, body: data.to_json) do |response| self._decompress(response.body_io, response.headers["Content-Encoding"]?) end From c27bb90e4d8286665658e2805a26ad8881472618 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 9 Apr 2024 16:26:16 -0400 Subject: [PATCH 553/598] Add support for new comment format --- src/invidious/comments/youtube.cr | 199 +++++++++++++++--------- src/invidious/routes/api/v1/channels.cr | 2 +- src/invidious/routes/channels.cr | 2 +- 3 files changed, 130 insertions(+), 73 deletions(-) diff --git a/src/invidious/comments/youtube.cr b/src/invidious/comments/youtube.cr index 185d8e43..375672d7 100644 --- a/src/invidious/comments/youtube.cr +++ b/src/invidious/comments/youtube.cr @@ -57,7 +57,7 @@ module Invidious::Comments return initial_data end - def parse_youtube(id, response, format, locale, thin_mode, sort_by = "top", isPost = false) + def parse_youtube(id, response, format, locale, thin_mode, sort_by = "top", is_post = false) contents = nil if on_response_received_endpoints = response["onResponseReceivedEndpoints"]? @@ -113,7 +113,7 @@ module Invidious::Comments json.field "commentCount", comment_count end - if isPost + if is_post json.field "postId", id else json.field "videoId", id @@ -131,89 +131,147 @@ module Invidious::Comments node_replies = node["replies"]["commentRepliesRenderer"] end - if node["comment"]? - node_comment = node["comment"]["commentRenderer"] + if node["commentViewModel"]? + cvm = node.dig("commentViewModel", "commentViewModel") + comment_key = cvm["commentKey"] + toolbar_key = cvm["toolbarStateKey"] + if mutations = response.dig?("frameworkUpdates", "entityBatchUpdate", "mutations") + comment_mutation = mutations.as_a.find { |i| i.dig?("payload", "commentEntityPayload", "key") == comment_key} + toolbar_mutation = mutations.as_a.find { |i| i.dig?("entityKey") == toolbar_key} + if !comment_mutation.nil? && !toolbar_mutation.nil? + html_content = comment_mutation.dig("payload", "commentEntityPayload", "properties", "content", "content").as_s + if comment_author = comment_mutation.dig?("payload", "commentEntityPayload", "author") + json.field "authorId", comment_author["channelId"].as_s + json.field "authorUrl", "/channel/#{comment_author["channelId"].as_s}" + json.field "author", comment_author["displayName"].as_s + json.field "verified", comment_author["isVerified"].as_bool + json.field "authorThumbnails" do + json.array do + comment_mutation.dig?("payload", "commentEntityPayload", "avatar", "image", "sources").try &.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 + json.field "authorIsChannelOwner", comment_author["isCreator"].as_bool + json.field "isSponsor", (comment_author["sponsorBadgeUrl"]?!= nil) + if comment_author["sponsorBadgeUrl"]? + # Sponsor icon thumbnails always have one object and there's only ever the url property in it + json.field "sponsorIconUrl", comment_author["sponsorBadgeUrl"].to_s + end + end + + if comment_toolbar = comment_mutation.dig?("payload", "commentEntityPayload", "toolbar") + json.field "likeCount", short_text_to_number(comment_toolbar["likeCountNotliked"].as_s) + json.field "replyCount", short_text_to_number(comment_toolbar["replyCount"]?.try &.as_s || "0") + if heart_state = toolbar_mutation.dig?("payload", "engagementToolbarStateEntityPayload", "heartState") + if heart_state.as_s == "TOOLBAR_HEART_STATE_HEARTED" + json.field "creatorHeart" do + json.object do + json.field "creatorThumbnail", comment_toolbar["creatorThumbnailUrl"].as_s + json.field "creatorName", comment_toolbar["heartActiveTooltip"].as_s.sub("❤ by ", "") + end + end + end + end + published_text = comment_mutation.dig?("payload", "commentEntityPayload", "properties", "publishedTime").try &.as_s + end + end + end + json.field "isPinned", (cvm.dig?("pinnedText") != nil) + json.field "isSponsored", false + json.field "commentId", cvm["commentId"] else - node_comment = node["commentRenderer"] - end + if node["comment"]? + node_comment = node["comment"]["commentRenderer"] + else + node_comment = node["commentRenderer"] + end + json.field "commentId", node_comment["commentId"] + html_content = node_comment["contentText"]?.try { |t| parse_content(t, id) } - 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 "verified", (node_comment["authorCommentBadge"]? != nil) + json.field "author", node_comment["authorText"]?.try &.["simpleText"]? || "" + 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 - json.field "author", author - json.field "authorThumbnails" do - json.array do - node_comment["authorThumbnail"]["thumbnails"].as_a.each do |thumbnail| + if comment_action_buttons_renderer = node_comment.dig?("actionButtons", "commentActionButtonsRenderer") + json.field "likeCount", comment_action_buttons_renderer["likeButton"]["toggleButtonRenderer"]["accessibilityData"]["accessibilityData"]["label"].as_s.scan(/\d/).map(&.[0]).join.to_i + if comment_action_buttons_renderer["creatorHeart"]? + heart_data = comment_action_buttons_renderer["creatorHeart"]["creatorHeartRenderer"]["creatorThumbnail"] + json.field "creatorHeart" do + json.object do + json.field "creatorThumbnail", heart_data["thumbnails"][-1]["url"] + json.field "creatorName", heart_data["accessibility"]["accessibilityData"]["label"] + 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 + + json.field "authorIsChannelOwner", node_comment["authorIsChannelOwner"] + json.field "isPinned", (node_comment["pinnedCommentBadge"]? != nil) + published_text = node_comment["publishedTimeText"]["runs"][0]["text"].as_s + + 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 + + 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 "url", thumbnail["url"] - json.field "width", thumbnail["width"] - json.field "height", thumbnail["height"] + json.field "replyCount", node_comment["replyCount"]? || 1 + json.field "continuation", continuation 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 - + content_html = html_content || "" 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 + if published_text != nil + published_text = published_text.to_s + if published_text.includes?(" (edited)") + json.field "isEdited", true + published = decode_date(published_text.rchop(" (edited)")) + else + json.field "isEdited", false + published = decode_date(published_text) 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 + json.field "published", published.to_unix + json.field "publishedText", translate(locale, "`x` ago", recode_date(published, locale)) end end end @@ -236,7 +294,6 @@ module Invidious::Comments if format == "html" response = JSON.parse(response) content_html = Frontend::Comments.template_youtube(response, locale, thin_mode) - response = JSON.build do |json| json.object do json.field "contentHtml", content_html diff --git a/src/invidious/routes/api/v1/channels.cr b/src/invidious/routes/api/v1/channels.cr index 67018660..c6be8b06 100644 --- a/src/invidious/routes/api/v1/channels.cr +++ b/src/invidious/routes/api/v1/channels.cr @@ -393,7 +393,7 @@ module Invidious::Routes::API::V1::Channels else comments = YoutubeAPI.browse(continuation: continuation) end - return Comments.parse_youtube(id, comments, format, locale, thin_mode, isPost: true) + return Comments.parse_youtube(id, comments, format, locale, thin_mode, is_post: true) end def self.channels(env) diff --git a/src/invidious/routes/channels.cr b/src/invidious/routes/channels.cr index d4d8b1c1..fea49bbe 100644 --- a/src/invidious/routes/channels.cr +++ b/src/invidious/routes/channels.cr @@ -231,7 +231,7 @@ module Invidious::Routes::Channels if nojs comments = Comments.fetch_community_post_comments(ucid, id) - comment_html = JSON.parse(Comments.parse_youtube(id, comments, "html", locale, thin_mode, isPost: true))["contentHtml"] + comment_html = JSON.parse(Comments.parse_youtube(id, comments, "html", locale, thin_mode, is_post: true))["contentHtml"] end templated "post" end From a9f55aa31062e148bd0fa15636a004762acabedd Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 9 Apr 2024 17:06:36 -0400 Subject: [PATCH 554/598] fix lint, improve performance --- src/invidious/comments/youtube.cr | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/invidious/comments/youtube.cr b/src/invidious/comments/youtube.cr index 375672d7..4c6a0d56 100644 --- a/src/invidious/comments/youtube.cr +++ b/src/invidious/comments/youtube.cr @@ -104,6 +104,8 @@ module Invidious::Comments end end + mutations = response.dig?("frameworkUpdates", "entityBatchUpdate", "mutations").try &.as_a || [] of JSON::Any + response = JSON.build do |json| json.object do if header @@ -135,9 +137,9 @@ module Invidious::Comments cvm = node.dig("commentViewModel", "commentViewModel") comment_key = cvm["commentKey"] toolbar_key = cvm["toolbarStateKey"] - if mutations = response.dig?("frameworkUpdates", "entityBatchUpdate", "mutations") - comment_mutation = mutations.as_a.find { |i| i.dig?("payload", "commentEntityPayload", "key") == comment_key} - toolbar_mutation = mutations.as_a.find { |i| i.dig?("entityKey") == toolbar_key} + if mutations.size != 0 + comment_mutation = mutations.find { |i| i.dig?("payload", "commentEntityPayload", "key") == comment_key } + toolbar_mutation = mutations.find { |i| i.dig?("entityKey") == toolbar_key } if !comment_mutation.nil? && !toolbar_mutation.nil? html_content = comment_mutation.dig("payload", "commentEntityPayload", "properties", "content", "content").as_s if comment_author = comment_mutation.dig?("payload", "commentEntityPayload", "author") @@ -156,8 +158,8 @@ module Invidious::Comments end end end - json.field "authorIsChannelOwner", comment_author["isCreator"].as_bool - json.field "isSponsor", (comment_author["sponsorBadgeUrl"]?!= nil) + json.field "authorIsChannelOwner", comment_author["isCreator"].as_bool + json.field "isSponsor", (comment_author["sponsorBadgeUrl"]? != nil) if comment_author["sponsorBadgeUrl"]? # Sponsor icon thumbnails always have one object and there's only ever the url property in it json.field "sponsorIconUrl", comment_author["sponsorBadgeUrl"].to_s From 039212ed9199ebcac7686bdb1c562c86d708cfc9 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 9 Apr 2024 18:04:21 -0400 Subject: [PATCH 555/598] escape html, add todo comment --- src/invidious/comments/youtube.cr | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/invidious/comments/youtube.cr b/src/invidious/comments/youtube.cr index 4c6a0d56..ee1568e5 100644 --- a/src/invidious/comments/youtube.cr +++ b/src/invidious/comments/youtube.cr @@ -141,7 +141,8 @@ module Invidious::Comments comment_mutation = mutations.find { |i| i.dig?("payload", "commentEntityPayload", "key") == comment_key } toolbar_mutation = mutations.find { |i| i.dig?("entityKey") == toolbar_key } if !comment_mutation.nil? && !toolbar_mutation.nil? - html_content = comment_mutation.dig("payload", "commentEntityPayload", "properties", "content", "content").as_s + # todo parse styleRuns, commandRuns and attachmentRuns for comments + html_content = HTML.escape(comment_mutation.dig("payload", "commentEntityPayload", "properties", "content", "content").as_s) if comment_author = comment_mutation.dig?("payload", "commentEntityPayload", "author") json.field "authorId", comment_author["channelId"].as_s json.field "authorUrl", "/channel/#{comment_author["channelId"].as_s}" From de2287963ff48acf40f719be7ef1de615e799ffd Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Wed, 10 Apr 2024 18:21:42 -0400 Subject: [PATCH 556/598] fix loading replies to comments, remove unneeded code Co-Authored-By: Samantaz Fox --- src/invidious/comments/youtube.cr | 116 +++++++++++++++--------------- 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/src/invidious/comments/youtube.cr b/src/invidious/comments/youtube.cr index ee1568e5..ecf86ede 100644 --- a/src/invidious/comments/youtube.cr +++ b/src/invidious/comments/youtube.cr @@ -134,58 +134,56 @@ module Invidious::Comments end if node["commentViewModel"]? - cvm = node.dig("commentViewModel", "commentViewModel") + # two commentViewModels for inital request + cvm = node.dig?("commentViewModel", "commentViewModel") + # one commentViewModel when getting a replies to a comment + cvm ||= node.dig("commentViewModel") comment_key = cvm["commentKey"] toolbar_key = cvm["toolbarStateKey"] - if mutations.size != 0 - comment_mutation = mutations.find { |i| i.dig?("payload", "commentEntityPayload", "key") == comment_key } - toolbar_mutation = mutations.find { |i| i.dig?("entityKey") == toolbar_key } - if !comment_mutation.nil? && !toolbar_mutation.nil? - # todo parse styleRuns, commandRuns and attachmentRuns for comments - html_content = HTML.escape(comment_mutation.dig("payload", "commentEntityPayload", "properties", "content", "content").as_s) - if comment_author = comment_mutation.dig?("payload", "commentEntityPayload", "author") - json.field "authorId", comment_author["channelId"].as_s - json.field "authorUrl", "/channel/#{comment_author["channelId"].as_s}" - json.field "author", comment_author["displayName"].as_s - json.field "verified", comment_author["isVerified"].as_bool - json.field "authorThumbnails" do - json.array do - comment_mutation.dig?("payload", "commentEntityPayload", "avatar", "image", "sources").try &.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 + comment_mutation = mutations.find { |i| i.dig?("payload", "commentEntityPayload", "key") == comment_key } + toolbar_mutation = mutations.find { |i| i.dig?("entityKey") == toolbar_key } + if !comment_mutation.nil? && !toolbar_mutation.nil? + # todo parse styleRuns, commandRuns and attachmentRuns for comments + html_content = HTML.escape(comment_mutation.dig("payload", "commentEntityPayload", "properties", "content", "content").as_s) + comment_author = comment_mutation.dig("payload", "commentEntityPayload", "author") + json.field "authorId", comment_author["channelId"].as_s + json.field "authorUrl", "/channel/#{comment_author["channelId"].as_s}" + json.field "author", comment_author["displayName"].as_s + json.field "verified", comment_author["isVerified"].as_bool + json.field "authorThumbnails" do + json.array do + comment_mutation.dig?("payload", "commentEntityPayload", "avatar", "image", "sources").try &.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 - json.field "authorIsChannelOwner", comment_author["isCreator"].as_bool - json.field "isSponsor", (comment_author["sponsorBadgeUrl"]? != nil) - if comment_author["sponsorBadgeUrl"]? - # Sponsor icon thumbnails always have one object and there's only ever the url property in it - json.field "sponsorIconUrl", comment_author["sponsorBadgeUrl"].to_s - end end - - if comment_toolbar = comment_mutation.dig?("payload", "commentEntityPayload", "toolbar") - json.field "likeCount", short_text_to_number(comment_toolbar["likeCountNotliked"].as_s) - json.field "replyCount", short_text_to_number(comment_toolbar["replyCount"]?.try &.as_s || "0") - if heart_state = toolbar_mutation.dig?("payload", "engagementToolbarStateEntityPayload", "heartState") - if heart_state.as_s == "TOOLBAR_HEART_STATE_HEARTED" - json.field "creatorHeart" do - json.object do - json.field "creatorThumbnail", comment_toolbar["creatorThumbnailUrl"].as_s - json.field "creatorName", comment_toolbar["heartActiveTooltip"].as_s.sub("❤ by ", "") - end - end - end - end - published_text = comment_mutation.dig?("payload", "commentEntityPayload", "properties", "publishedTime").try &.as_s + json.field "authorIsChannelOwner", comment_author["isCreator"].as_bool + json.field "isSponsor", (comment_author["sponsorBadgeUrl"]? != nil) + if sponsor_badge_url = comment_author["sponsorBadgeUrl"]? + # Sponsor icon thumbnails always have one object and there's only ever the url property in it + json.field "sponsorIconUrl", sponsor_badge_url end end + + comment_toolbar = comment_mutation.dig("payload", "commentEntityPayload", "toolbar") + json.field "likeCount", short_text_to_number(comment_toolbar["likeCountNotliked"].as_s) + reply_count = short_text_to_number(comment_toolbar["replyCount"]?.try &.as_s || "0") + if heart_state = toolbar_mutation.dig?("payload", "engagementToolbarStateEntityPayload", "heartState") + if heart_state.as_s == "TOOLBAR_HEART_STATE_HEARTED" + json.field "creatorHeart" do + json.object do + json.field "creatorThumbnail", comment_toolbar["creatorThumbnailUrl"].as_s + json.field "creatorName", comment_toolbar["heartActiveTooltip"].as_s.sub("❤ by ", "") + end + end + end + end + published_text = comment_mutation.dig?("payload", "commentEntityPayload", "properties", "publishedTime").try &.as_s end json.field "isPinned", (cvm.dig?("pinnedText") != nil) - json.field "isSponsored", false json.field "commentId", cvm["commentId"] else if node["comment"]? @@ -242,21 +240,7 @@ module Invidious::Comments json.field "sponsorIconUrl", node_comment.dig("sponsorCommentBadge", "sponsorCommentBadgeRenderer", "customBadge", "thumbnails", 0, "url").to_s 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 + reply_count = node_comment["replyCount"]? end content_html = html_content || "" @@ -276,6 +260,22 @@ module Invidious::Comments json.field "published", published.to_unix json.field "publishedText", translate(locale, "`x` ago", recode_date(published, locale)) 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", reply_count || 1 + json.field "continuation", continuation + end + end + end end end end From fbf07e18aae6a8cc8863051c2b7ecf8cae341898 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Wed, 10 Apr 2024 21:31:39 -0400 Subject: [PATCH 557/598] Parse links in the comments Co-Authored-By: Samantaz Fox --- src/invidious/comments/content.cr | 16 ++++++++-------- src/invidious/comments/youtube.cr | 2 +- src/invidious/videos/description.cr | 14 +++++++++++++- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/invidious/comments/content.cr b/src/invidious/comments/content.cr index c8cdc2df..beefd9ad 100644 --- a/src/invidious/comments/content.cr +++ b/src/invidious/comments/content.cr @@ -64,15 +64,15 @@ def content_to_comment_html(content, video_id : String? = "") # check for custom emojis if run["emoji"]? if run["emoji"]["isCustomEmoji"]?.try &.as_bool - if emojiImage = run.dig?("emoji", "image") - emojiAlt = emojiImage.dig?("accessibility", "accessibilityData", "label").try &.as_s || text - emojiThumb = emojiImage["thumbnails"][0] + if emoji_image = run.dig?("emoji", "image") + emoji_alt = emoji_image.dig?("accessibility", "accessibilityData", "label").try &.as_s || text + emoji_thumb = emoji_image["thumbnails"][0] text = String.build do |str| - str << %() << emojiAlt << ) end else diff --git a/src/invidious/comments/youtube.cr b/src/invidious/comments/youtube.cr index ecf86ede..3d624325 100644 --- a/src/invidious/comments/youtube.cr +++ b/src/invidious/comments/youtube.cr @@ -144,7 +144,7 @@ module Invidious::Comments toolbar_mutation = mutations.find { |i| i.dig?("entityKey") == toolbar_key } if !comment_mutation.nil? && !toolbar_mutation.nil? # todo parse styleRuns, commandRuns and attachmentRuns for comments - html_content = HTML.escape(comment_mutation.dig("payload", "commentEntityPayload", "properties", "content", "content").as_s) + html_content = parse_description(comment_mutation.dig("payload", "commentEntityPayload", "properties", "content"), id) comment_author = comment_mutation.dig("payload", "commentEntityPayload", "author") json.field "authorId", comment_author["channelId"].as_s json.field "authorUrl", "/channel/#{comment_author["channelId"].as_s}" diff --git a/src/invidious/videos/description.cr b/src/invidious/videos/description.cr index 542cb416..c7191dec 100644 --- a/src/invidious/videos/description.cr +++ b/src/invidious/videos/description.cr @@ -7,7 +7,19 @@ private def copy_string(str : String::Builder, iter : Iterator, count : Int) : I cp = iter.next break if cp.is_a?(Iterator::Stop) - str << cp.chr + if cp == 0x26 # Ampersand (&) + str << "&" + elsif cp == 0x27 # Single quote (') + str << "'" + elsif cp == 0x22 # Double quote (") + str << """ + elsif cp == 0x3C # Less-than (<) + str << "<" + elsif cp == 0x3E # Greater than (>) + str << ">" + else + str << cp.chr + end # A codepoint from the SMP counts twice copied += 1 if cp > 0xFFFF From d1eae101472303eb09c929aa9a508289d8befb46 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 16 Apr 2024 18:21:45 -0400 Subject: [PATCH 558/598] make `authorVerified` a bool value --- src/invidious/jsonify/api_v1/video_json.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/jsonify/api_v1/video_json.cr b/src/invidious/jsonify/api_v1/video_json.cr index ed912ff3..a189dc57 100644 --- a/src/invidious/jsonify/api_v1/video_json.cr +++ b/src/invidious/jsonify/api_v1/video_json.cr @@ -227,7 +227,7 @@ module Invidious::JSONify::APIv1 json.field "author", rv["author"] json.field "authorUrl", "/channel/#{rv["ucid"]?}" json.field "authorId", rv["ucid"]? - json.field "authorVerified", rv["author_verified"] + json.field "authorVerified", rv["author_verified"] == "true" if rv["author_thumbnail"]? json.field "authorThumbnails" do json.array do From 2b6e71b5531f887580920bda964dc0fc68556aa4 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Sat, 20 Apr 2024 10:04:27 -0400 Subject: [PATCH 559/598] Simplify cvm assignment logic + improve formatting Co-Authored-By: Samantaz Fox --- src/invidious/comments/youtube.cr | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/invidious/comments/youtube.cr b/src/invidious/comments/youtube.cr index 3d624325..0716fcde 100644 --- a/src/invidious/comments/youtube.cr +++ b/src/invidious/comments/youtube.cr @@ -133,15 +133,16 @@ module Invidious::Comments node_replies = node["replies"]["commentRepliesRenderer"] end - if node["commentViewModel"]? + if cvm = node["commentViewModel"]? # two commentViewModels for inital request - cvm = node.dig?("commentViewModel", "commentViewModel") # one commentViewModel when getting a replies to a comment - cvm ||= node.dig("commentViewModel") + cvm = cvm["commentViewModel"] if cvm["commentViewModel"]? + comment_key = cvm["commentKey"] toolbar_key = cvm["toolbarStateKey"] comment_mutation = mutations.find { |i| i.dig?("payload", "commentEntityPayload", "key") == comment_key } toolbar_mutation = mutations.find { |i| i.dig?("entityKey") == toolbar_key } + if !comment_mutation.nil? && !toolbar_mutation.nil? # todo parse styleRuns, commandRuns and attachmentRuns for comments html_content = parse_description(comment_mutation.dig("payload", "commentEntityPayload", "properties", "content"), id) @@ -160,17 +161,20 @@ module Invidious::Comments end end end - json.field "authorIsChannelOwner", comment_author["isCreator"].as_bool - json.field "isSponsor", (comment_author["sponsorBadgeUrl"]? != nil) - if sponsor_badge_url = comment_author["sponsorBadgeUrl"]? - # Sponsor icon thumbnails always have one object and there's only ever the url property in it - json.field "sponsorIconUrl", sponsor_badge_url - end + end + + json.field "authorIsChannelOwner", comment_author["isCreator"].as_bool + json.field "isSponsor", (comment_author["sponsorBadgeUrl"]? != nil) + + if sponsor_badge_url = comment_author["sponsorBadgeUrl"]? + # Sponsor icon thumbnails always have one object and there's only ever the url property in it + json.field "sponsorIconUrl", sponsor_badge_url end comment_toolbar = comment_mutation.dig("payload", "commentEntityPayload", "toolbar") json.field "likeCount", short_text_to_number(comment_toolbar["likeCountNotliked"].as_s) reply_count = short_text_to_number(comment_toolbar["replyCount"]?.try &.as_s || "0") + if heart_state = toolbar_mutation.dig?("payload", "engagementToolbarStateEntityPayload", "heartState") if heart_state.as_s == "TOOLBAR_HEART_STATE_HEARTED" json.field "creatorHeart" do @@ -181,8 +185,10 @@ module Invidious::Comments end end end + published_text = comment_mutation.dig?("payload", "commentEntityPayload", "properties", "publishedTime").try &.as_s end + json.field "isPinned", (cvm.dig?("pinnedText") != nil) json.field "commentId", cvm["commentId"] else From f313162fa1080bc4797dbf11ee44f51cc4c57985 Mon Sep 17 00:00:00 2001 From: absidue <48293849+absidue@users.noreply.github.com> Date: Sun, 21 Apr 2024 12:53:31 +0200 Subject: [PATCH 560/598] Add bitrate to formatStreams in /api/v1/videos/{id} response --- src/invidious/jsonify/api_v1/video_json.cr | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/invidious/jsonify/api_v1/video_json.cr b/src/invidious/jsonify/api_v1/video_json.cr index 1651559a..eec163f2 100644 --- a/src/invidious/jsonify/api_v1/video_json.cr +++ b/src/invidious/jsonify/api_v1/video_json.cr @@ -160,6 +160,8 @@ module Invidious::JSONify::APIv1 json.field "type", fmt["mimeType"] json.field "quality", fmt["quality"] + json.field "bitrate", fmt["bitrate"].as_i.to_s if fmt["bitrate"]? + fmt_info = Invidious::Videos::Formats.itag_to_metadata?(fmt["itag"]) if fmt_info fps = fmt_info["fps"]?.try &.to_i || fmt["fps"]?.try &.as_i || 30 From 24de19d06f35cd21a92c7f18869c376ddc170acc Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 23 Apr 2024 23:31:47 -0400 Subject: [PATCH 561/598] only ignore smaller trending categories on default trending tab --- src/invidious/trending.cr | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/invidious/trending.cr b/src/invidious/trending.cr index 2d9f8a83..107d148d 100644 --- a/src/invidious/trending.cr +++ b/src/invidious/trending.cr @@ -22,12 +22,14 @@ def fetch_trending(trending_type, region, locale) extracted = [] of SearchItem + deduplicate = items.size > 1 + items.each do |itm| if itm.is_a?(Category) # Ignore the smaller categories, as they generally contain a sponsored # channel, which brings a lot of noise on the trending page. # See: https://github.com/iv-org/invidious/issues/2989 - next if itm.contents.size < 24 + next if (itm.contents.size < 24 && deduplicate) extracted.concat extract_category(itm) else From f7ae680c2570f97a3336ab49d0fdf95efc8f3e95 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 25 Apr 2024 18:35:08 +0200 Subject: [PATCH 562/598] Update Turkish translation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update Turkish translation Update Turkish translation Co-authored-by: Hosted Weblate Co-authored-by: Oğuz Ersen --- locales/tr.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/locales/tr.json b/locales/tr.json index d25cfd65..3b7bf3a4 100644 --- a/locales/tr.json +++ b/locales/tr.json @@ -21,7 +21,7 @@ "Import and Export Data": "Verileri İçe ve Dışa Aktar", "Import": "İçe Aktar", "Import Invidious data": "Invidious JSON Verilerini İçe Aktar", - "Import YouTube subscriptions": "YouTube/OPML Aboneliklerini İçe Aktar", + "Import YouTube subscriptions": "YouTube CSV veya OPML Aboneliklerini İçe Aktar", "Import FreeTube subscriptions (.db)": "FreeTube Aboneliklerini İçe Aktar (.db)", "Import NewPipe subscriptions (.json)": "NewPipe Aboneliklerini İçe Aktar (.json)", "Import NewPipe data (.zip)": "NewPipe Verilerini İçe Aktar (.zip)", @@ -488,5 +488,13 @@ "generic_channels_count": "{{count}} kanal", "generic_channels_count_plural": "{{count}} kanal", "Import YouTube watch history (.json)": "YouTube İzleme Geçmişini İçe Aktar (.json)", - "toggle_theme": "Temayı Değiştir" + "toggle_theme": "Temayı Değiştir", + "Add to playlist": "Oynatma listesine ekle", + "Add to playlist: ": "Oynatma listesine ekle: ", + "Answer": "Yanıt", + "Search for videos": "Video ara", + "carousel_slide": "Sunum {{current}} / {{total}}", + "carousel_skip": "Kayar menüyü atla", + "carousel_go_to": "`x` sunumuna git", + "The Popular feed has been disabled by the administrator.": "Popüler akışı yönetici tarafından devre dışı bırakıldı." } From 668c130f01ad4707ed6480115674308e982c3bed Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 25 Apr 2024 18:35:08 +0200 Subject: [PATCH 563/598] Update Turkmen translation Add Turkmen translation Co-authored-by: Hosted Weblate Co-authored-by: Hydyr Sopyyew --- locales/tk.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 locales/tk.json diff --git a/locales/tk.json b/locales/tk.json new file mode 100644 index 00000000..798ea6ce --- /dev/null +++ b/locales/tk.json @@ -0,0 +1,7 @@ +{ + "Add to playlist": "Aýdym sanawyna goş", + "Add to playlist: ": "Pleýliste goş: ", + "Answer": "Jogap", + "Search for videos": "Wideo gözläň", + "The Popular feed has been disabled by the administrator.": "Trende bolan administrator tarapyndan ýapyldy." +} From e92d250a1c4ec6d09186cc5dcc0074e6ef8742a1 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 25 Apr 2024 18:35:08 +0200 Subject: [PATCH 564/598] Update Portuguese (Brazil) translation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update Portuguese (Brazil) translation Update Portuguese (Brazil) translation Update Portuguese (Brazil) translation Update Portuguese (Brazil) translation Update Portuguese (Brazil) translation Update Portuguese (Brazil) translation Update Portuguese (Brazil) translation Update Portuguese (Brazil) translation Update Portuguese (Brazil) translation Update Portuguese (Brazil) translation Update Portuguese (Brazil) translation Co-authored-by: André Marcelo Alvarenga Co-authored-by: Hosted Weblate Co-authored-by: Jose Delvani Co-authored-by: joaooliva --- locales/pt-BR.json | 264 +++++++++++++++++++++++---------------------- 1 file changed, 136 insertions(+), 128 deletions(-) diff --git a/locales/pt-BR.json b/locales/pt-BR.json index af14eb29..1637b5d8 100644 --- a/locales/pt-BR.json +++ b/locales/pt-BR.json @@ -1,27 +1,27 @@ { "LIVE": "AO VIVO", - "Shared `x` ago": "Compartilhado `x` atrás", + "Shared `x` ago": "Publicado há `x`", "Unsubscribe": "Cancelar inscrição", "Subscribe": "Inscrever-se", "View channel on YouTube": "Ver canal no YouTube", - "View playlist on YouTube": "Ver lista de reprodução no YouTube", + "View playlist on YouTube": "Ver playlist no YouTube", "newest": "mais recentes", "oldest": "mais antigos", "popular": "populares", - "last": "último", + "last": "últimos", "Next page": "Próxima página", "Previous page": "Página anterior", - "Clear watch history?": "Limpar histórico de reprodução?", + "Clear watch history?": "Limpar histórico de exibição?", "New password": "Nova senha", - "New passwords must match": "Nova senha deve ser igual", - "Authorize token?": "Autorizar o token?", - "Authorize token for `x`?": "Autorizar o token para `x`?", + "New passwords must match": "As senhas devem ser iguais", + "Authorize token?": "Autorizar token?", + "Authorize token for `x`?": "Autorizar token para `x`?", "Yes": "Sim", "No": "Não", - "Import and Export Data": "Importar e Exportar Dados", + "Import and Export Data": "Importar/exportar dados", "Import": "Importar", - "Import Invidious data": "Importar dados em JSON do Invidious", - "Import YouTube subscriptions": "Importar inscrições do YouTube/OPML", + "Import Invidious data": "Importar dados JSON do Invidious", + "Import YouTube subscriptions": "Importar inscrições no formato CSV ou OPML do YouTube", "Import FreeTube subscriptions (.db)": "Importar inscrições do FreeTube (.db)", "Import NewPipe subscriptions (.json)": "Importar inscrições do NewPipe (.json)", "Import NewPipe data (.zip)": "Importar dados do NewPipe (.zip)", @@ -32,49 +32,49 @@ "Delete account?": "Excluir conta?", "History": "Histórico", "An alternative front-end to YouTube": "Uma interface alternativa para o YouTube", - "JavaScript license information": "Informação de licença do JavaScript", - "source": "código-fonte", - "Log in": "Entrar", - "Log in/register": "Entrar/Registrar", + "JavaScript license information": "Informações sobre a licença do JavaScript", + "source": "fonte", + "Log in": "Fazer login", + "Log in/register": "Fazer login/criar conta", "User ID": "Usuário", "Password": "Senha", "Time (h:mm:ss):": "Hora (h:mm:ss):", - "Text CAPTCHA": "CAPTCHA em texto", - "Image CAPTCHA": "CAPTCHA em imagem", + "Text CAPTCHA": "Mudar para um desafio de texto", + "Image CAPTCHA": "Mudar para um desafio visual", "Sign In": "Entrar", - "Register": "Registrar", + "Register": "Criar conta", "E-mail": "E-mail", "Preferences": "Preferências", - "preferences_category_player": "Preferências do reprodutor", + "preferences_category_player": "Preferências de reprodução", "preferences_video_loop_label": "Repetir sempre: ", "preferences_autoplay_label": "Reprodução automática: ", - "preferences_continue_label": "Sempre reproduzir próximo: ", + "preferences_continue_label": "Reproduzir a seguir, por padrão: ", "preferences_continue_autoplay_label": "Reproduzir próximo vídeo automaticamente: ", "preferences_listen_label": "Apenas áudio por padrão: ", "preferences_local_label": "Usar proxy nos vídeos: ", "preferences_speed_label": "Velocidade padrão: ", "preferences_quality_label": "Qualidade de vídeo preferida: ", "preferences_volume_label": "Volume de reprodução: ", - "preferences_comments_label": "Preferência de comentários: ", + "preferences_comments_label": "Comentários padrão: ", "youtube": "YouTube", "reddit": "Reddit", - "preferences_captions_label": "Preferência de legendas: ", + "preferences_captions_label": "Legendas padrão: ", "Fallback captions: ": "Legendas alternativas: ", "preferences_related_videos_label": "Mostrar vídeos relacionados: ", "preferences_annotations_label": "Sempre mostrar anotações: ", - "preferences_extend_desc_label": "Estenda automaticamente a descrição do vídeo: ", + "preferences_extend_desc_label": "Expandir automaticamente a descrição do vídeo: ", "preferences_vr_mode_label": "Vídeos interativos de 360 graus (requer WebGL): ", "preferences_category_visual": "Preferências visuais", - "preferences_player_style_label": "Estilo do tocador: ", + "preferences_player_style_label": "Estilo de reprodução: ", "Dark mode: ": "Modo escuro: ", "preferences_dark_mode_label": "Tema: ", "dark": "escuro", "light": "claro", "preferences_thin_mode_label": "Modo compacto: ", "preferences_category_misc": "Preferências diversas", - "preferences_automatic_instance_redirect_label": "Redirecionamento de instância automática (fallback para redirect.invidious.io): ", + "preferences_automatic_instance_redirect_label": "Redirecionamento automático de instâncias (alternativa para redirect.invidious.io): ", "preferences_category_subscription": "Preferências de inscrições", - "preferences_annotations_subscribed_label": "Sempre mostrar anotações dos vídeos de canais inscritos: ", + "preferences_annotations_subscribed_label": "Mostrar anotações por padrão para canais inscritos? ", "Redirect homepage to feed: ": "Redirecionar página inicial para o feed: ", "preferences_max_results_label": "Número de vídeos no feed: ", "preferences_sort_label": "Ordenar vídeos por: ", @@ -84,30 +84,30 @@ "alphabetically - reverse": "alfabética - ordem inversa", "channel name": "nome do canal", "channel name - reverse": "nome do canal - ordem inversa", - "Only show latest video from channel: ": "Mostrar apenas o vídeo mais recente do canal: ", - "Only show latest unwatched video from channel: ": "Mostrar apenas o vídeo mais recente não visualizado do canal: ", - "preferences_unseen_only_label": "Mostrar apenas vídeos não visualizados: ", - "preferences_notifications_only_label": "Mostrar apenas notificações (se existentes): ", - "Enable web notifications": "Ativar notificações pela web", - "`x` uploaded a video": "`x` publicou um novo vídeo", + "Only show latest video from channel: ": "Mostrar apenas vídeos mais recentes do canal: ", + "Only show latest unwatched video from channel: ": "Mostrar apenas vídeos mais recentes não assistido do canal: ", + "preferences_unseen_only_label": "Mostrar apenas vídeos não assistido: ", + "preferences_notifications_only_label": "Mostrar apenas notificações (se houver): ", + "Enable web notifications": "Ativar notificações da Web", + "`x` uploaded a video": "`x` publicou um vídeo", "`x` is live": "`x` está ao vivo", "preferences_category_data": "Preferências de dados", - "Clear watch history": "Limpar histórico de reprodução", - "Import/export data": "Importar/Exportar dados", + "Clear watch history": "Limpar histórico de exibição", + "Import/export data": "Importar/exportar dados", "Change password": "Alterar senha", "Manage subscriptions": "Gerenciar inscrições", "Manage tokens": "Gerenciar tokens", - "Watch history": "Histórico de reprodução", - "Delete account": "Apagar sua conta", + "Watch history": "Histórico de exibição", + "Delete account": "Excluir conta", "preferences_category_admin": "Preferências de administrador", - "preferences_default_home_label": "Página de início padrão: ", - "preferences_feed_menu_label": "Menu do feed: ", - "preferences_show_nick_label": "Mostrar o nickname no topo: ", - "Top enabled: ": "Habilitar destaques: ", - "CAPTCHA enabled: ": "Habilitar CAPTCHA: ", - "Login enabled: ": "Habilitar login: ", - "Registration enabled: ": "Habilitar registro: ", - "Report statistics: ": "Habilitar estatísticas: ", + "preferences_default_home_label": "Página inicial padrão: ", + "preferences_feed_menu_label": "Guias de feed preferidos: ", + "preferences_show_nick_label": "Mostrar nome de usuário na parte superior: ", + "Top enabled: ": "Destaques ativados: ", + "CAPTCHA enabled: ": "CAPTCHA ativado: ", + "Login enabled: ": "Fazer login ativado: ", + "Registration enabled: ": "Criar conta ativado: ", + "Report statistics: ": "Relatório de estatísticas: ", "Save preferences": "Salvar preferências", "Subscription manager": "Gerenciador de inscrições", "Token manager": "Gerenciador de tokens", @@ -115,24 +115,24 @@ "tokens_count_0": "{{count}} token", "tokens_count_1": "{{count}} tokens", "tokens_count_2": "{{count}} tokens", - "Import/export": "Importar/Exportar", + "Import/export": "Importar/exportar", "unsubscribe": "cancelar inscrição", "revoke": "revogar", "Subscriptions": "Inscrições", - "search": "Pesquisar", + "search": "pesquisar", "Log out": "Sair", "Released under the AGPLv3 on Github.": "Lançado sob a AGPLv3 no GitHub.", "Source available here.": "Código-fonte disponível aqui.", - "View JavaScript license information.": "Ver informações da licença do JavaScript.", - "View privacy policy.": "Ver a política de privacidade.", - "Trending": "Tendências", + "View JavaScript license information.": "Informações de licença JavaScript.", + "View privacy policy.": "Política de privacidade.", + "Trending": "Em alta", "Public": "Público", "Unlisted": "Não listado", "Private": "Privado", - "View all playlists": "Mostrar todas listas de reprodução", + "View all playlists": "Ver todas as playlists", "Updated `x` ago": "Atualizado `x` atrás", - "Delete playlist `x`?": "Apagar a playlist `x`?", - "Delete playlist": "Apagar playlist", + "Delete playlist `x`?": "Excluir playlist `x`?", + "Delete playlist": "Excluir playlist", "Create playlist": "Criar playlist", "Title": "Título", "Playlist privacy": "Privacidade da playlist", @@ -140,24 +140,24 @@ "Show more": "Mostrar mais", "Show less": "Mostrar menos", "Watch on YouTube": "Assistir no YouTube", - "Switch Invidious Instance": "Mudar a instância do Invidious", + "Switch Invidious Instance": "Alterar instância Invidious", "Hide annotations": "Ocultar anotações", "Show annotations": "Mostrar anotações", "Genre: ": "Gênero: ", "License: ": "Licença: ", "Family friendly? ": "Filtrar conteúdo impróprio: ", "Wilson score: ": "Pontuação de Wilson: ", - "Engagement: ": "Empenho: ", + "Engagement: ": "Engajamento: ", "Whitelisted regions: ": "Regiões permitidas: ", "Blacklisted regions: ": "Regiões bloqueadas: ", - "Shared `x`": "Compartilhado `x`", + "Shared `x`": "Publicado em `x`", "Premieres in `x`": "Estreia em `x`", "Premieres `x`": "Estreia `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.": "Oi! Parece que seu JavaScript está desativado. Clique aqui para ver os comentários, entretanto eles podem levar um pouco mais de tempo para carregar.", + "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Olá! Parece que você está com o JavaScript desativado. Clique aqui para ver os comentários, mas lembre-se de que eles podem demorar um pouco mais para carregar.", "View YouTube comments": "Ver comentários no YouTube", "View more comments on Reddit": "Ver mais comentários no Reddit", "View `x` comments": { - "([^.,0-9]|^)1([^.,0-9]|$)": "Ver `x` comentários", + "([^.,0-9]|^)1([^.,0-9]|$)": "Ver `x` comentário", "": "Ver `x` comentários" }, "View Reddit comments": "Ver comentários no Reddit", @@ -166,7 +166,7 @@ "Incorrect password": "Senha incorreta", "Wrong answer": "Resposta incorreta", "Erroneous CAPTCHA": "CAPTCHA inválido", - "CAPTCHA is a required field": "O CAPTCHA é um campo obrigatório", + "CAPTCHA is a required field": "CAPTCHA é um campo obrigatório", "User ID is a required field": "O nome de usuário é um campo obrigatório", "Password is a required field": "A senha é um campo obrigatório", "Wrong username or password": "Nome de usuário ou senha inválidos", @@ -175,17 +175,17 @@ "Please log in": "Por favor, inicie sua sessão", "Invidious Private Feed for `x`": "Feed Privado do Invidious para `x`", "channel:`x`": "canal: `x`", - "Deleted or invalid channel": "Este canal foi apagado ou é inválido", + "Deleted or invalid channel": "Canal excluído ou inválido", "This channel does not exist.": "Este canal não existe.", "Could not get channel info.": "Não foi possível obter as informações do canal.", "Could not fetch comments": "Não foi possível obter os comentários", "`x` ago": "`x` atrás", "Load more": "Carregar mais", "Could not create mix.": "Não foi possível criar o mix.", - "Empty playlist": "Lista de reprodução vazia", - "Not a playlist.": "Não é uma lista de reprodução.", - "Playlist does not exist.": "A lista de reprodução não existe.", - "Could not pull trending pages.": "Não foi possível obter as páginas dos vídeos em alta.", + "Empty playlist": "Playlist vazia", + "Not a playlist.": "Não é uma playlist.", + "Playlist does not exist.": "A playlist não existe.", + "Could not pull trending pages.": "Não foi possível obter as páginas de vídeos em alta.", "Hidden field \"challenge\" is a required field": "O campo oculto \"desafio\" é obrigatório", "Hidden field \"token\" is a required field": "O campo oculto \"token\" é obrigatório", "Erroneous challenge": "Desafio inválido", @@ -319,87 +319,87 @@ "generic_count_seconds_0": "{{count}} segundo", "generic_count_seconds_1": "{{count}} segundos", "generic_count_seconds_2": "{{count}} segundos", - "Fallback comments: ": "Comentários alternativos: ", + "Fallback comments: ": "Alternativa para comentários: ", "Popular": "Populares", - "Search": "Procurar", - "Top": "No topo", + "Search": "Pesquisar", + "Top": "Destaques", "About": "Sobre", "Rating: ": "Avaliação: ", "preferences_locale_label": "Idioma: ", - "View as playlist": "Ver como lista de reprodução", + "View as playlist": "Ver como playlist", "Default": "Padrão", "Music": "Músicas", "Gaming": "Jogos", "News": "Notícias", "Movies": "Filmes", - "Download": "Baixar", + "Download": "Download", "Download as: ": "Baixar como: ", "%A %B %-d, %Y": "%A %-d %B %Y", "(edited)": "(editado)", "YouTube comment permalink": "Link permanente do comentário no YouTube", "permalink": "Link permanente", - "`x` marked it with a ❤": "`x` foi marcado como ❤", + "`x` marked it with a ❤": "`x` foi marcado com um ❤", "Audio mode": "Modo de áudio", "Video mode": "Modo de vídeo", "channel_tab_videos_label": "Vídeos", - "Playlists": "Listas de reprodução", + "Playlists": "Playlists", "channel_tab_community_label": "Comunidade", - "search_filters_sort_option_relevance": "relevância", - "search_filters_sort_option_rating": "avaliação", - "search_filters_sort_option_date": "data", - "search_filters_sort_option_views": "visualizações", - "search_filters_type_label": "content_type", - "search_filters_duration_label": "duração", - "search_filters_features_label": "recursos", - "search_filters_sort_label": "ordenar", - "search_filters_date_option_hour": "hora", - "search_filters_date_option_today": "hoje", - "search_filters_date_option_week": "semana", - "search_filters_date_option_month": "mês", - "search_filters_date_option_year": "ano", - "search_filters_type_option_video": "vídeo", + "search_filters_sort_option_relevance": "Relevância", + "search_filters_sort_option_rating": "Avaliação", + "search_filters_sort_option_date": "Data de publicação", + "search_filters_sort_option_views": "Visualizações", + "search_filters_type_label": "Tipo", + "search_filters_duration_label": "Duração", + "search_filters_features_label": "Características", + "search_filters_sort_label": "Ordenar por", + "search_filters_date_option_hour": "Últimas horas", + "search_filters_date_option_today": "Hoje", + "search_filters_date_option_week": "Esta semana", + "search_filters_date_option_month": "Este mês", + "search_filters_date_option_year": "Este ano", + "search_filters_type_option_video": "Vídeo", "search_filters_type_option_channel": "Canal", - "search_filters_type_option_playlist": "playlist", - "search_filters_type_option_movie": "filme", - "search_filters_type_option_show": "show", - "search_filters_features_option_hd": "hd", - "search_filters_features_option_subtitles": "legendas", - "search_filters_features_option_c_commons": "creative_commons", - "search_filters_features_option_three_d": "3d", - "search_filters_features_option_live": "ao vivo", - "search_filters_features_option_four_k": "4k", - "search_filters_features_option_location": "localização", - "search_filters_features_option_hdr": "hdr", + "search_filters_type_option_playlist": "Playlist", + "search_filters_type_option_movie": "Filme", + "search_filters_type_option_show": "Séries", + "search_filters_features_option_hd": "HD", + "search_filters_features_option_subtitles": "Legendas", + "search_filters_features_option_c_commons": "Creative Commons", + "search_filters_features_option_three_d": "3D", + "search_filters_features_option_live": "AO VIVO", + "search_filters_features_option_four_k": "4K", + "search_filters_features_option_location": "Localização", + "search_filters_features_option_hdr": "HDR", "Current version: ": "Versão atual: ", "next_steps_error_message": "Depois disso, você deve tentar: ", - "next_steps_error_message_refresh": "Atualizar", + "next_steps_error_message_refresh": "Recarregar", "next_steps_error_message_go_to_youtube": "Ir para o YouTube", - "footer_donate_page": "Doe", - "adminprefs_modified_source_code_url_label": "URL para repositório de código fonte modificado", + "footer_donate_page": "Doar", + "adminprefs_modified_source_code_url_label": "URL para o repositório do código-fonte modificado", "search_filters_duration_option_long": "Longo (> 20 minutos)", "search_filters_duration_option_short": "Curto (< 4 minutos)", "footer_documentation": "Documentação", - "footer_source_code": "Código fonte", - "footer_original_source_code": "Código fonte original", + "footer_source_code": "Código-fonte", + "footer_original_source_code": "Código-fonte original", "footer_modfied_source_code": "Código-fonte modificado", - "preferences_quality_dash_label": "Qualidade de vídeo do painel preferida: ", + "preferences_quality_dash_label": "Qualidade de vídeo DASH preferida: ", "preferences_region_label": "País do conteúdo: ", "preferences_quality_dash_option_4320p": "4320p", "generic_videos_count_0": "{{count}} vídeo", "generic_videos_count_1": "{{count}} vídeos", "generic_videos_count_2": "{{count}} vídeos", - "generic_playlists_count_0": "{{count}} lista de reprodução", - "generic_playlists_count_1": "{{count}} listas de reprodução", - "generic_playlists_count_2": "{{count}} listas de reprodução", + "generic_playlists_count_0": "{{count}} playlist", + "generic_playlists_count_1": "{{count}} playlists", + "generic_playlists_count_2": "{{count}} playlists", "generic_subscribers_count_0": "{{count}} inscrito", "generic_subscribers_count_1": "{{count}} inscritos", "generic_subscribers_count_2": "{{count}} inscritos", "generic_subscriptions_count_0": "{{count}} inscrição", "generic_subscriptions_count_1": "{{count}} inscrições", "generic_subscriptions_count_2": "{{count}} inscrições", - "subscriptions_unseen_notifs_count_0": "{{count}} notificação não vista", - "subscriptions_unseen_notifs_count_1": "{{count}} notificações não vistas", - "subscriptions_unseen_notifs_count_2": "{{count}} notificações não vistas", + "subscriptions_unseen_notifs_count_0": "{{count}} notificação não visualizada", + "subscriptions_unseen_notifs_count_1": "{{count}} notificações não visualizadas", + "subscriptions_unseen_notifs_count_2": "{{count}} notificações não visualizadas", "comments_view_x_replies_0": "Ver {{count}} resposta", "comments_view_x_replies_1": "Ver {{count}} respostas", "comments_view_x_replies_2": "Ver {{count}} respostas", @@ -407,14 +407,14 @@ "comments_points_count_1": "{{count}} pontos", "comments_points_count_2": "{{count}} pontos", "crash_page_you_found_a_bug": "Parece que você encontrou um erro no Invidious!", - "crash_page_before_reporting": "Antes de reportar um erro, verifique se você:", - "preferences_save_player_pos_label": "Salvar a posição de reprodução: ", + "crash_page_before_reporting": "Antes de informar um erro, verifique se você:", + "preferences_save_player_pos_label": "Salvar posição de reprodução: ", "search_filters_features_option_purchased": "Comprado", "crash_page_refresh": "tentou recarregar a página", "crash_page_switch_instance": "tentou usar outra instância", "crash_page_search_issue": "procurou por um erro existente no GitHub", "crash_page_report_issue": "Se nenhuma opção acima ajudou, por favor abra um novo problema no Github (preferencialmente em inglês) e inclua o seguinte texto (NÃO traduza):", - "crash_page_read_the_faq": "leia as Perguntas frequentes (FAQ)", + "crash_page_read_the_faq": "leu as Perguntas frequentes (FAQ)", "generic_views_count_0": "{{count}} visualização", "generic_views_count_1": "{{count}} visualizações", "generic_views_count_2": "{{count}} visualizações", @@ -422,8 +422,8 @@ "preferences_quality_option_hd720": "HD720", "preferences_quality_option_small": "Pequeno", "preferences_quality_dash_option_auto": "Auto", - "preferences_quality_dash_option_best": "Melhor", - "preferences_quality_dash_option_worst": "Pior", + "preferences_quality_dash_option_best": "Melhor qualidade", + "preferences_quality_dash_option_worst": "Pior qualidade", "preferences_quality_dash_option_2160p": "2160p", "preferences_quality_dash_option_1440p": "1440p", "preferences_quality_dash_option_1080p": "1080p", @@ -435,17 +435,17 @@ "invidious": "Invidious", "preferences_quality_option_medium": "Médio", "search_filters_features_option_three_sixty": "360°", - "none": "none", + "none": "nenhum", "videoinfo_watch_on_youTube": "Assistir no YouTube", - "videoinfo_youTube_embed_link": "Embutir", - "videoinfo_invidious_embed_link": "Link Embutido", + "videoinfo_youTube_embed_link": "Embed", + "videoinfo_invidious_embed_link": "Embed link", "download_subtitles": "Legendas - `x` (.vtt)", - "user_created_playlists": "`x` listas de reprodução criadas", - "user_saved_playlists": "`x` listas de reprodução salvas", + "user_created_playlists": "`x` playlists criadas", + "user_saved_playlists": "`x` playlists salvas", "Video unavailable": "Vídeo indisponível", "videoinfo_started_streaming_x_ago": "Iniciou a transmissão a `x`", "search_filters_title": "Filtro", - "preferences_watch_history_label": "Ative o histórico de exibição: ", + "preferences_watch_history_label": "Ativar histórico de exibição: ", "search_message_no_results": "Nenhum resultado encontrado.", "search_message_change_filters_or_query": "Tente ampliar sua consulta de pesquisa e/ou alterar os filtros.", "English (United Kingdom)": "Inglês (Reino Unido)", @@ -465,7 +465,7 @@ "Portuguese (Brazil)": "Português (Brasil)", "Russian (auto-generated)": "Russo (gerado automaticamente)", "Vietnamese (auto-generated)": "Vietnamita (gerado automaticamente)", - "search_filters_date_label": "Data de upload", + "search_filters_date_label": "Data de publicação", "search_filters_date_option_none": "Qualquer data", "Dutch (auto-generated)": "Holandês (gerado automaticamente)", "French (auto-generated)": "Francês (gerado automaticamente)", @@ -479,21 +479,21 @@ "Turkish (auto-generated)": "Turco (gerado automaticamente)", "search_filters_duration_option_medium": "Médio (4 - 20 minutos)", "search_filters_features_option_vr180": "VR180", - "Popular enabled: ": "Popular habilitado: ", + "Popular enabled: ": "Página \"Populares\" ativada: ", "error_video_not_in_playlist": "O vídeo solicitado não existe nesta playlist. Clique aqui para acessar a página inicial da playlist.", "channel_tab_channels_label": "Canais", - "channel_tab_playlists_label": "Listas de reprodução", - "channel_tab_shorts_label": "Curtos", - "channel_tab_streams_label": "Ao Vivo", + "channel_tab_playlists_label": "Playlists", + "channel_tab_shorts_label": "Shorts", + "channel_tab_streams_label": "Transmissão ao vivo", "Music in this video": "Música neste vídeo", "Artist: ": "Artista: ", "Album: ": "Álbum: ", "Standard YouTube license": "Licença padrão do YouTube", "Song: ": "Música: ", - "Channel Sponsor": "Patrocinador do Canal", - "Download is disabled": "Download está desabilitado", - "Import YouTube playlist (.csv)": "Importar lista de reprodução do YouTube (.csv)", - "generic_button_delete": "Apagar", + "Channel Sponsor": "Patrocinador do canal", + "Download is disabled": "Download indisponível", + "Import YouTube playlist (.csv)": "Importar playlist do YouTube (.csv)", + "generic_button_delete": "Excluir", "generic_button_save": "Salvar", "generic_button_edit": "Editar", "playlist_button_add_items": "Adicionar vídeos", @@ -504,6 +504,14 @@ "generic_channels_count_0": "{{count}} canal", "generic_channels_count_1": "{{count}} canais", "generic_channels_count_2": "{{count}} canais", - "Import YouTube watch history (.json)": "Importar histórico de reprodução do YouTube (.json)", - "toggle_theme": "Alternar Tema" + "Import YouTube watch history (.json)": "Importar histórico de exibição do YouTube (.json)", + "toggle_theme": "Alternar tema", + "Add to playlist": "Adicionar à playlist", + "Add to playlist: ": "Adicionar à playlist: ", + "Search for videos": "Pesquisar vídeos", + "The Popular feed has been disabled by the administrator.": "O feed \"Populares\" foi desativado pelo administrador.", + "Answer": "Resposta", + "carousel_slide": "Slide {{current}} de {{total}}", + "carousel_skip": "Ignorar carrossel", + "carousel_go_to": "Ir ao slide `x`" } From 89c008211d86a2e047d144d12e5c762550348c37 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 25 Apr 2024 18:35:08 +0200 Subject: [PATCH 565/598] Update German translation Co-authored-by: Hosted Weblate Co-authored-by: Lenny Angst --- locales/de.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/locales/de.json b/locales/de.json index 756aff76..46327f57 100644 --- a/locales/de.json +++ b/locales/de.json @@ -487,5 +487,11 @@ "channel_tab_releases_label": "Veröffentlichungen", "generic_channels_count": "{{count}} Kanal", "generic_channels_count_plural": "{{count}} Kanäle", - "Import YouTube watch history (.json)": "YouTube Wiedergabeverlauf importieren (.json)" + "Import YouTube watch history (.json)": "YouTube Wiedergabeverlauf importieren (.json)", + "Answer": "Antwort", + "The Popular feed has been disabled by the administrator.": "Der Angesagt-Feed wurde vom Administrator deaktiviert.", + "Add to playlist": "Einer Wiedergabeliste hinzufügen", + "Search for videos": "Nach Videos suchen", + "toggle_theme": "Thema wechseln", + "Add to playlist: ": "Einer Wiedergabeliste hinzufügen: " } From a2f9707b3f3085ad16f5f61abf7e232951f409bb Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 25 Apr 2024 18:35:08 +0200 Subject: [PATCH 566/598] Update Danish translation Co-authored-by: Samantaz Fox --- locales/da.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/da.json b/locales/da.json index 019f1c51..9cbb446a 100644 --- a/locales/da.json +++ b/locales/da.json @@ -165,12 +165,12 @@ "Password cannot be empty": "Adgangskoden må ikke være tom", "Password cannot be longer than 55 characters": "Adgangskoden må ikke være længere end 55 tegn", "Please log in": "Venligst log ind", - "channel:`x`": "kanal: 'x'", + "channel:`x`": "kanal: `x`", "Deleted or invalid channel": "Slettet eller invalid kanal", "This channel does not exist.": "Denne kanal eksisterer ikke.", "Could not get channel info.": "Kunne ikke hente kanal info.", "Could not fetch comments": "Kunne ikke hente kommentarer", - "`x` ago": "'x' siden", + "`x` ago": "`x` siden", "Load more": "Hent flere", "Could not create mix.": "Kunne ikke skabe blanding.", "Empty playlist": "Tom playliste", From 25cbfd068143f778b9154a83a082273b33a62e9b Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 25 Apr 2024 18:35:08 +0200 Subject: [PATCH 567/598] Update Basque translation Co-authored-by: Samantaz Fox --- locales/eu.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/locales/eu.json b/locales/eu.json index 8b365270..fbca537b 100644 --- a/locales/eu.json +++ b/locales/eu.json @@ -161,13 +161,13 @@ "Source available here.": "Iturburua hemen eskura.", "View JavaScript license information.": "JavaScriptaren lizentzi adierazpena ikusi.", "Blacklisted regions: ": "zerrenda beltzaren zonaldeak: ", - "Premieres `x`": "'x' estrenaldiak", + "Premieres `x`": "`x` estrenaldiak", "Wrong answer": "Erantzun ez zuzena", "Password is a required field": "Pasahitza beharrezkoa da", "Wrong username or password": "Pasahitza edo ezizena gaizki", "Password cannot be longer than 55 characters": "Pasahitza 55 karaktere baino luzeagoa ezin da izan", "This channel does not exist.": "Kanal hau ez dago.", - "`x` ago": "duela 'x'", + "`x` ago": "duela `x`", "Czech": "Txekiera", "preferences_region_label": "Herrialdeko edukiera: ", "preferences_sort_label": "Bideoak ordenatu: ", @@ -207,24 +207,24 @@ "Public": "Orokorra", "Unlisted": "Ez zerrendatua", "Subscription manager": "Harpidetzen kudeatzailea", - "Updated `x` ago": "Duela 'x' eguneratua", + "Updated `x` ago": "Duela `x` eguneratua", "Hide replies": "Erantzunak izkutatu", "preferences_thin_mode_label": "Urri eran: ", "Show replies": "Erantzunak erakutsi", "Watch on YouTube": "YouTuben ikusi", - "Premieres in `x`": "'x'eko estrenaldiak", - "Delete playlist `x`?": "'x' zerrenda ezabatu nahi?", + "Premieres in `x`": "`x`eko estrenaldiak", + "Delete playlist `x`?": "`x` zerrenda ezabatu nahi?", "Token is expired, please try again": "Token kadukatua, saiatu berriro", "CAPTCHA enabled: ": "CAPTCHA gaitu: ", "Released under the AGPLv3 on Github.": "GitHubeko AGPLv3pean argitaratuta.", - "channel:`x`": "Kanal: 'x'", + "channel:`x`": "Kanal: `x`", "Georgian": "Georgiera", "Incorrect password": "Pasahitza gaizki", "Playlist does not exist.": "Zerrenda ez da existitzen.", "preferences_category_misc": "Askotariko lehentasunak", "View `x` comments": { - "([^.,0-9]|^)1([^.,0-9]|$)": "'x' iruzkina ikusi", - "": "'x' iruzkinak ikusi" + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` iruzkina ikusi", + "": "`x` iruzkinak ikusi" }, "Report statistics: ": "Estatistikak adierazi: ", "preferences_max_results_label": "Jotzeko bideo zerrendaren luzera: ", @@ -237,7 +237,7 @@ "Hidden field \"challenge\" is a required field": "\"challenge\" eremu ezkutua beharrezkoa da", "German": "Alemaniarra", "View YouTube comments": "YouTubeko iruzkinak ikusi", - "`x` is live": "'x' bizirik darrai", + "`x` is live": "`x` bizirik darrai", "Password cannot be empty": "Pasahitza ezin da hutsik utzi", "preferences_video_loop_label": "Beti begiztatu: ", "Only show latest unwatched video from channel: ": "kanalaren azken bideo ezikusia erakutsi soilik ", @@ -261,9 +261,9 @@ "Hide annotations": "Oharrak izkutatu", "Title": "Titulua", "channel name": "Kanalaren izena", - "Authorize token for `x`?": "Baimendu tokena 'x'tzako?", + "Authorize token for `x`?": "Baimendu tokena `x`tzako?", "Private": "Pribatua", - "Editing playlist `x`": "'x' zerrenda editatu", + "Editing playlist `x`": "`x` zerrenda editatu", "Could not pull trending pages.": "Ezin ekarri orri arrakastatsuak.", "crash_page_read_the_faq": "Bide (FAQ) ohiko galderak" } From 066b1c35cc350134d94b1b548ae36bafcb153e63 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 25 Apr 2024 18:35:08 +0200 Subject: [PATCH 568/598] Update Romanian translation Co-authored-by: Hosted Weblate Co-authored-by: Wiktor Muzynski --- locales/ro.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/ro.json b/locales/ro.json index 85bf746f..ccbeef63 100644 --- a/locales/ro.json +++ b/locales/ro.json @@ -478,5 +478,6 @@ "search_filters_type_option_all": "orice tip", "preferences_quality_dash_option_240p": "240p", "preferences_quality_dash_option_144p": "144p", - "Show less": "Afișați mai puțin" + "Show less": "Afișați mai puțin", + "Add to playlist": "Adaugă la playlist" } From cbbaded209e7a5008658b448847d597a5aad68e9 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 25 Apr 2024 18:35:08 +0200 Subject: [PATCH 569/598] Update Bengali translation Co-authored-by: Hosted Weblate Co-authored-by: Tauhid Alam Rifty --- locales/bn.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/locales/bn.json b/locales/bn.json index 9d1c7b24..501a1ca3 100644 --- a/locales/bn.json +++ b/locales/bn.json @@ -90,5 +90,7 @@ "preferences_quality_option_medium": "মধ্যম", "preferences_quality_option_small": "ছোট", "preferences_quality_dash_option_1080p": "১০৮০পি", - "preferences_quality_dash_option_720p": "৭২০পি" + "preferences_quality_dash_option_720p": "৭২০পি", + "Add to playlist": "প্লেলিস্টে যোগ করুন", + "Add to playlist: ": "প্লেলিস্টে যোগ করুন: " } From 197b3972a93e98096059a1931c3d1d267a801ff1 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 25 Apr 2024 18:35:08 +0200 Subject: [PATCH 570/598] Update Ukrainian translation Update Ukrainian translation Co-authored-by: Hosted Weblate Co-authored-by: Ihor Hordiichuk Co-authored-by: Samantaz Fox --- locales/uk.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/locales/uk.json b/locales/uk.json index f9640bba..223772d9 100644 --- a/locales/uk.json +++ b/locales/uk.json @@ -127,7 +127,7 @@ "Create playlist": "Створити список відтворення", "Title": "Заголовок", "Playlist privacy": "Конфіденційність списку відтворення", - "Editing playlist `x`": "Редагування списку відтворення \"x\"", + "Editing playlist `x`": "Редагування списку відтворення `x`", "Watch on YouTube": "Дивитися на YouTube", "Hide annotations": "Приховати анотації", "Show annotations": "Показати анотації", @@ -505,5 +505,13 @@ "generic_channels_count_1": "{{count}} канали", "generic_channels_count_2": "{{count}} каналів", "Import YouTube watch history (.json)": "Імпортувати історію переглядів YouTube (.json)", - "toggle_theme": "Перемкнути тему" + "toggle_theme": "Перемкнути тему", + "Add to playlist": "Додати до списку відтворення", + "Add to playlist: ": "Додати до списку відтворення: ", + "Answer": "Відповідь", + "Search for videos": "Шукати відео", + "The Popular feed has been disabled by the administrator.": "Стрічка Популярні вимкнена адміністратором.", + "carousel_slide": "Слайд {{current}} з {{total}}", + "carousel_skip": "Пропустити карусель", + "carousel_go_to": "Перейти до слайда `x`" } From dd01b0f5eb48eae2cc76be31b93d2b2b78436856 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 25 Apr 2024 18:35:08 +0200 Subject: [PATCH 571/598] Update Japanese translation Co-authored-by: Hosted Weblate Co-authored-by: maboroshin --- locales/ja.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/locales/ja.json b/locales/ja.json index 2e3437bc..d430b2a4 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -470,5 +470,14 @@ "generic_button_rss": "RSS", "playlist_button_add_items": "動画を追加", "generic_channels_count_0": "{{count}}個のチャンネル", - "Import YouTube watch history (.json)": "YouTube 視聴履歴をインポート (.json)" + "Import YouTube watch history (.json)": "YouTube 視聴履歴をインポート (.json)", + "Add to playlist": "再生リストに追加", + "Add to playlist: ": "再生リストに追加: ", + "Answer": "回答", + "Search for videos": "動画を検索", + "The Popular feed has been disabled by the administrator.": "人気の動画のページは管理者によって無効にされています。", + "carousel_go_to": "スライド`x`を表示", + "carousel_slide": "スライド{{current}} / 全{{total}}個中", + "carousel_skip": "画像のスライド表示をスキップ", + "toggle_theme": "テーマの切り替え" } From 97c4263530524b5d325a8728d7c07b9f668aa112 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 25 Apr 2024 18:35:08 +0200 Subject: [PATCH 572/598] Update Czech translation Update Czech translation Co-authored-by: Fjuro Co-authored-by: Hosted Weblate --- locales/cs.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/locales/cs.json b/locales/cs.json index 4aa20f28..1350f146 100644 --- a/locales/cs.json +++ b/locales/cs.json @@ -21,7 +21,7 @@ "Import and Export Data": "Import a export dat", "Import": "Importovat", "Import Invidious data": "Importovat JSON údaje Invidious", - "Import YouTube subscriptions": "Importovat odběry z YouTube/OPML", + "Import YouTube subscriptions": "Importovat odběry z YouTube CSV nebo OPML", "Import FreeTube subscriptions (.db)": "Importovat odběry z FreeTube (.db)", "Import NewPipe subscriptions (.json)": "Importovat odběry z NewPipe (.json)", "Import NewPipe data (.zip)": "Importovat údeje z NewPipe (.zip)", @@ -505,5 +505,13 @@ "generic_channels_count_1": "{{count}} kanály", "generic_channels_count_2": "{{count}} kanálů", "Import YouTube watch history (.json)": "Importovat historii sledování z YouTube (.json)", - "toggle_theme": "Přepnout motiv" + "toggle_theme": "Přepnout motiv", + "Add to playlist": "Přidat do playlistu", + "Add to playlist: ": "Přidat do playlistu: ", + "Answer": "Odpověď", + "Search for videos": "Hledat videa", + "The Popular feed has been disabled by the administrator.": "Kategorie Populární byla zakázána administrátorem.", + "carousel_slide": "Snímek {{current}} z {{total}}", + "carousel_skip": "Přeskočit galerii", + "carousel_go_to": "Přejít na snímek `x`" } From a6bcf0280c08031c6f792944e8e67cdbc8d070cb Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 25 Apr 2024 18:35:08 +0200 Subject: [PATCH 573/598] Update Portuguese translation Update Portuguese translation Update Portuguese translation Co-authored-by: Hosted Weblate Co-authored-by: Samantaz Fox Co-authored-by: Sergio Marques --- locales/pt.json | 168 +++++++++++++++++++++++++----------------------- 1 file changed, 88 insertions(+), 80 deletions(-) diff --git a/locales/pt.json b/locales/pt.json index c1d8b5b4..463dbf3a 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -1,25 +1,25 @@ { - "search_filters_type_option_show": "Série", + "search_filters_type_option_show": "Séries", "search_filters_sort_option_views": "Visualizações", "search_filters_sort_option_date": "Data de carregamento", "search_filters_sort_option_rating": "Avaliação", "search_filters_sort_option_relevance": "Relevância", - "Switch Invidious Instance": "Mudar a instância do Invidious", + "Switch Invidious Instance": "Alterar instância Invidious", "Show less": "Mostrar menos", "Show more": "Mostrar mais", - "Released under the AGPLv3 on Github.": "Lançado sob a AGPLv3 no GitHub.", + "Released under the AGPLv3 on Github.": "Disponibilizada sob a AGPLv3 no GitHub.", "preferences_show_nick_label": "Mostrar nome de utilizador em cima: ", "preferences_automatic_instance_redirect_label": "Redirecionamento de instância automática (solução de último recurso para redirect.invidious.io): ", "preferences_category_misc": "Preferências diversas", - "preferences_vr_mode_label": "Vídeos interativos de 360 graus (necessita de WebGL): ", - "preferences_extend_desc_label": "Estender automaticamente a descrição do vídeo: ", + "preferences_vr_mode_label": "Vídeos interativos de 360 graus (requer WebGL): ", + "preferences_extend_desc_label": "Expandir automaticamente a descrição do vídeo: ", "next_steps_error_message_go_to_youtube": "Ir para o YouTube", "next_steps_error_message": "Pode tentar as seguintes opções: ", - "next_steps_error_message_refresh": "Atualizar", + "next_steps_error_message_refresh": "Recarregar", "search_filters_features_option_hdr": "HDR", "search_filters_features_option_location": "Localização", "search_filters_features_option_four_k": "4K", - "search_filters_features_option_live": "Ao Vivo", + "search_filters_features_option_live": "Direto", "search_filters_features_option_three_d": "3D", "search_filters_features_option_c_commons": "Creative Commons", "search_filters_features_option_subtitles": "Legendas", @@ -37,11 +37,11 @@ "search_filters_features_label": "Funcionalidades", "search_filters_duration_label": "Duração", "search_filters_type_label": "Tipo", - "permalink": "hiperligação permanente", - "YouTube comment permalink": "Hiperligação permanente do comentário no YouTube", + "permalink": "ligação permanente", + "YouTube comment permalink": "Ligação permanente do comentário no YouTube", "Download as: ": "Descarregar como: ", "Download": "Descarregar", - "Default": "Predefinido", + "Default": "Padrão", "Top": "Destaques", "Search": "Pesquisar", "generic_count_years_0": "{{count}} ano", @@ -67,21 +67,21 @@ "generic_count_seconds_2": "{{count}} segundos", "Chinese (Traditional)": "Chinês (tradicional)", "Chinese (Simplified)": "Chinês (simplificado)", - "Could not pull trending pages.": "Não foi possível obter as páginas de tendências.", - "Could not create mix.": "Não foi possível criar a mistura.", + "Could not pull trending pages.": "Não foi possível obter a página de tendências.", + "Could not create mix.": "Não foi possível criar o mix.", "Deleted or invalid channel": "Canal eliminado ou inválido", - "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Olá! Parece que o JavaScript está desativado. Clique aqui para ver os comentários, entretanto eles podem levar mais tempo para carregar.", + "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Olá! Parece que o JavaScript está desativado. Clique aqui para ver os comentários, mas tenha e conta que podem levar mais tempo para carregar.", "Delete playlist": "Eliminar lista de reprodução", - "Delete playlist `x`?": "Eliminar a lista de reprodução 'x'?", + "Delete playlist `x`?": "Eliminar lista de reprodução `x`?", "search": "pesquisar", "unsubscribe": "anular subscrição", - "Import/export": "Importar / exportar", + "Import/export": "Importar/exportar", "Save preferences": "Guardar preferências", "Top enabled: ": "Destaques ativados: ", "Delete account": "Eliminar conta", - "Import/export data": "Importar / exportar dados", + "Import/export data": "Importar/exportar dados", "preferences_annotations_label": "Mostrar anotações sempre: ", - "preferences_continue_label": "Reproduzir sempre o próximo: ", + "preferences_continue_label": "Reproduzir sempre o seguinte: ", "Sign In": "Entrar", "Log in/register": "Iniciar sessão/registar", "Delete account?": "Eliminar conta?", @@ -93,7 +93,7 @@ "Danish": "Dinamarquês", "Czech": "Checo", "Croatian": "Croata", - "Corsican": "Corso", + "Corsican": "Córsego", "Cebuano": "Cebuano", "Catalan": "Catalão", "Burmese": "Birmanês", @@ -107,10 +107,10 @@ "Arabic": "Árabe", "Amharic": "Amárico", "Albanian": "Albanês", - "Afrikaans": "Africano", + "Afrikaans": "Africânder", "English (auto-generated)": "Inglês (auto-gerado)", "English": "Inglês", - "Token is expired, please try again": "Token expirou, tente novamente", + "Token is expired, please try again": "Token caducado, tente novamente", "No such user": "Utilizador inválido", "Erroneous token": "Token inválido", "Erroneous challenge": "Desafio inválido", @@ -124,29 +124,29 @@ "Could not fetch comments": "Não foi possível obter os comentários", "Could not get channel info.": "Não foi possível obter as informações do canal.", "This channel does not exist.": "Este canal não existe.", - "channel:`x`": "canal:'x'", + "channel:`x`": "canal:`x`", "Invidious Private Feed for `x`": "Feed Privado do Invidious para `x`", "Please log in": "Por favor, inicie sessão", - "Password cannot be longer than 55 characters": "A palavra-chave não pode ser superior a 55 caracteres", - "Password cannot be empty": "A palavra-chave não pode estar vazia", - "Wrong username or password": "Nome de utilizador ou palavra-chave incorreto", - "Password is a required field": "Palavra-chave é um campo obrigatório", + "Password cannot be longer than 55 characters": "A palavra-passe não pode ter mais do que 55 caracteres", + "Password cannot be empty": "A palavra-passe não pode estar vazia", + "Wrong username or password": "Nome de utilizador ou palavra-passe incorreta", + "Password is a required field": "Palavra-passe é um campo obrigatório", "User ID is a required field": "O nome de utilizador é um campo obrigatório", "CAPTCHA is a required field": "CAPTCHA é um campo obrigatório", "Erroneous CAPTCHA": "CAPTCHA inválido", "Wrong answer": "Resposta errada", - "Incorrect password": "Palavra-chave incorreta", + "Incorrect password": "Palavra-passe incorreta", "Show replies": "Mostrar respostas", "Hide replies": "Ocultar respostas", "View Reddit comments": "Ver comentários do Reddit", "View `x` comments": { "": "Ver `x` comentários", - "([^.,0-9]|^)1([^.,0-9]|$)": "Ver `x` comentários" + "([^.,0-9]|^)1([^.,0-9]|$)": "Ver `x` comentário" }, "View more comments on Reddit": "Ver mais comentários no Reddit", "View YouTube comments": "Ver comentários do YouTube", - "Premieres `x`": "Estreias 'x'", - "Premieres in `x`": "Estreias em 'x'", + "Premieres `x`": "Estreia `x`", + "Premieres in `x`": "Estreia a `x`", "Shared `x`": "Partilhado `x`", "Blacklisted regions: ": "Regiões bloqueadas: ", "Whitelisted regions: ": "Regiões permitidas: ", @@ -158,44 +158,44 @@ "Show annotations": "Mostrar anotações", "Hide annotations": "Ocultar anotações", "Watch on YouTube": "Ver no YouTube", - "Editing playlist `x`": "A editar lista de reprodução 'x'", + "Editing playlist `x`": "A editar lista de reprodução `x`", "Playlist privacy": "Privacidade da lista de reprodução", "Title": "Título", "Create playlist": "Criar lista de reprodução", - "Updated `x` ago": "Atualizado `x` atrás", + "Updated `x` ago": "Atualizado há `x`", "View all playlists": "Ver todas as listas de reprodução", "Private": "Privado", "Unlisted": "Não listado", "Public": "Público", "Trending": "Tendências", - "View privacy policy.": "Ver a política de privacidade.", - "View JavaScript license information.": "Ver informações da licença do JavaScript.", + "View privacy policy.": "Ver política de privacidade.", + "View JavaScript license information.": "Ver informações da licença JavaScript.", "Source available here.": "Código-fonte disponível aqui.", "Log out": "Terminar sessão", "Subscriptions": "Subscrições", "revoke": "revogar", - "tokens_count_0": "{{count}} Token", - "tokens_count_1": "{{count}} Tokens", - "tokens_count_2": "{{count}} Tokens", + "tokens_count_0": "{{count}} token", + "tokens_count_1": "{{count}} tokens", + "tokens_count_2": "{{count}} tokens", "Token": "Token", - "Token manager": "Gerir tokens", - "Subscription manager": "Gerir subscrições", + "Token manager": "Gestor de tokens", + "Subscription manager": "Gestor de subscrições", "Report statistics: ": "Relatório de estatísticas: ", "Registration enabled: ": "Registar ativado: ", "Login enabled: ": "Iniciar sessão ativado: ", "CAPTCHA enabled: ": "CAPTCHA ativado: ", "preferences_feed_menu_label": "Menu de subscrições: ", - "preferences_default_home_label": "Página inicial predefinida: ", + "preferences_default_home_label": "Página inicial padrão: ", "preferences_category_admin": "Preferências de administrador", "Watch history": "Histórico de reprodução", "Manage tokens": "Gerir tokens", - "Manage subscriptions": "Gerir as subscrições", - "Change password": "Alterar palavra-chave", + "Manage subscriptions": "Gerir subscrições", + "Change password": "Alterar palavra-passe", "Clear watch history": "Limpar histórico de reprodução", "preferences_category_data": "Preferências de dados", "`x` is live": "`x` está em direto", - "`x` uploaded a video": "`x` publicou um novo vídeo", - "Enable web notifications": "Ativar notificações pela web", + "`x` uploaded a video": "`x` publicou um vídeo", + "Enable web notifications": "Ativar notificações web", "preferences_notifications_only_label": "Mostrar apenas notificações (se existirem): ", "preferences_unseen_only_label": "Mostrar apenas vídeos não visualizados: ", "Only show latest unwatched video from channel: ": "Mostrar apenas vídeos mais recentes não visualizados do canal: ", @@ -207,9 +207,9 @@ "published - reverse": "publicado - inverso", "published": "publicado", "preferences_sort_label": "Ordenar vídeos por: ", - "preferences_max_results_label": "Quantidade de vídeos nas subscrições: ", + "preferences_max_results_label": "Número de vídeos nas subscrições: ", "Redirect homepage to feed: ": "Redirecionar página inicial para subscrições: ", - "preferences_annotations_subscribed_label": "Mostrar sempre anotações aos canais subscritos: ", + "preferences_annotations_subscribed_label": "Mostrar sempre anotações nos canais subscritos: ", "preferences_category_subscription": "Preferências de subscrições", "preferences_thin_mode_label": "Modo compacto: ", "light": "claro", @@ -220,11 +220,11 @@ "preferences_category_visual": "Preferências visuais", "preferences_related_videos_label": "Mostrar vídeos relacionados: ", "Fallback captions: ": "Legendas alternativas: ", - "preferences_captions_label": "Legendas predefinidas: ", + "preferences_captions_label": "Legendas padrão: ", "reddit": "Reddit", "youtube": "YouTube", - "preferences_comments_label": "Preferência dos comentários: ", - "preferences_volume_label": "Volume da reprodução: ", + "preferences_comments_label": "Comentários padrão: ", + "preferences_volume_label": "Volume de reprodução: ", "preferences_quality_label": "Qualidade de vídeo preferida: ", "preferences_speed_label": "Velocidade preferida: ", "preferences_local_label": "Usar proxy nos vídeos: ", @@ -239,11 +239,11 @@ "Image CAPTCHA": "Imagem CAPTCHA", "Text CAPTCHA": "Texto CAPTCHA", "Time (h:mm:ss):": "Tempo (h:mm:ss):", - "Password": "Palavra-chave", + "Password": "Palavra-passe", "User ID": "Utilizador", "Log in": "Iniciar sessão", - "source": "código-fonte", - "JavaScript license information": "Informação de licença do JavaScript", + "source": "fonte", + "JavaScript license information": "Informação da licença JavaScript", "An alternative front-end to YouTube": "Uma interface alternativa ao YouTube", "History": "Histórico", "Export data as JSON": "Exportar dados Invidious como JSON", @@ -253,18 +253,18 @@ "Import NewPipe data (.zip)": "Importar dados do NewPipe (.zip)", "Import NewPipe subscriptions (.json)": "Importar subscrições do NewPipe (.json)", "Import FreeTube subscriptions (.db)": "Importar subscrições do FreeTube (.db)", - "Import YouTube subscriptions": "Importar subscrições do YouTube/OPML", + "Import YouTube subscriptions": "Importar subscrições via YouTube/OPML", "Import Invidious data": "Importar dados JSON do Invidious", "Import": "Importar", "No": "Não", "Yes": "Sim", - "Authorize token for `x`?": "Autorizar token para `x`?", - "Authorize token?": "Autorizar token?", - "New passwords must match": "As novas palavra-chaves devem corresponder", - "New password": "Nova palavra-chave", + "Authorize token for `x`?": "Autorizar 'token' para `x`?", + "Authorize token?": "Autorizar 'token'?", + "New passwords must match": "As novas palavras-passe devem ser iguais", + "New password": "Nova palavra-passe", "Clear watch history?": "Limpar histórico de reprodução?", "Previous page": "Página anterior", - "Next page": "Próxima página", + "Next page": "Página seguinte", "last": "últimos", "Current version: ": "Versão atual: ", "channel_tab_community_label": "Comunidade", @@ -272,19 +272,19 @@ "channel_tab_videos_label": "Vídeos", "Video mode": "Modo de vídeo", "Audio mode": "Modo de áudio", - "`x` marked it with a ❤": "`x` foi marcado como ❤", + "`x` marked it with a ❤": "`x` foi marcado com um ❤", "(edited)": "(editado)", "%A %B %-d, %Y": "%A %B %-d, %Y", "Movies": "Filmes", "News": "Notícias", "Gaming": "Jogos", - "Music": "Música", + "Music": "Músicas", "View as playlist": "Ver como lista de reprodução", "preferences_locale_label": "Idioma: ", "Rating: ": "Avaliação: ", - "About": "Sobre", + "About": "Acerca", "Popular": "Popular", - "Fallback comments: ": "Comentários alternativos: ", + "Fallback comments: ": "Alternativa para comentários: ", "Zulu": "Zulu", "Yoruba": "Ioruba", "Yiddish": "Iídiche", @@ -329,7 +329,7 @@ "Marathi": "Marathi", "Maori": "Maori", "Maltese": "Maltês", - "Malayalam": "Malaiala", + "Malayalam": "Malaialaio", "Malay": "Malaio", "Malagasy": "Malgaxe", "Macedonian": "Macedónio", @@ -365,15 +365,15 @@ "Galician": "Galego", "French": "Francês", "Finnish": "Finlandês", - "popular": "popular", - "oldest": "mais antigos", - "newest": "mais recentes", + "popular": "populares", + "oldest": "antigos", + "newest": "recentes", "View playlist on YouTube": "Ver lista de reprodução no YouTube", "View channel on YouTube": "Ver canal no YouTube", "Subscribe": "Subscrever", "Unsubscribe": "Anular subscrição", "Shared `x` ago": "Partilhado `x` atrás", - "LIVE": "AO VIVO", + "LIVE": "Direto", "search_filters_duration_option_short": "Curto (< 4 minutos)", "search_filters_duration_option_long": "Longo (> 20 minutos)", "footer_source_code": "Código-fonte", @@ -386,7 +386,7 @@ "preferences_quality_dash_label": "Qualidade de vídeo DASH preferida: ", "preferences_quality_option_small": "Baixa", "preferences_quality_option_hd720": "HD720", - "preferences_quality_dash_option_auto": "Automático", + "preferences_quality_dash_option_auto": "Automática", "preferences_quality_dash_option_best": "Melhor", "preferences_quality_dash_option_4320p": "4320p", "preferences_quality_dash_option_2160p": "2160p", @@ -397,7 +397,7 @@ "preferences_quality_dash_option_144p": "144p", "search_filters_features_option_purchased": "Comprado", "search_filters_features_option_three_sixty": "360°", - "videoinfo_invidious_embed_link": "Incorporar hiperligação", + "videoinfo_invidious_embed_link": "Incorporar ligação", "Video unavailable": "Vídeo não disponível", "invidious": "Invidious", "preferences_quality_option_medium": "Média", @@ -408,7 +408,7 @@ "preferences_quality_dash_option_worst": "Pior", "none": "nenhum", "videoinfo_youTube_embed_link": "Incorporar", - "preferences_save_player_pos_label": "Guardar a posição de reprodução atual do vídeo: ", + "preferences_save_player_pos_label": "Guardar posição de reprodução: ", "download_subtitles": "Legendas - `x` (.vtt)", "generic_views_count_0": "{{count}} visualização", "generic_views_count_1": "{{count}} visualizações", @@ -427,12 +427,12 @@ "comments_view_x_replies_0": "Ver {{count}} resposta", "comments_view_x_replies_1": "Ver {{count}} respostas", "comments_view_x_replies_2": "Ver {{count}} respostas", - "generic_subscribers_count_0": "{{count}} inscrito", - "generic_subscribers_count_1": "{{count}} inscritos", - "generic_subscribers_count_2": "{{count}} inscritos", - "generic_subscriptions_count_0": "{{count}} inscrição", - "generic_subscriptions_count_1": "{{count}} inscrições", - "generic_subscriptions_count_2": "{{count}} inscrições", + "generic_subscribers_count_0": "{{count}} subscritor", + "generic_subscribers_count_1": "{{count}} subscritores", + "generic_subscribers_count_2": "{{count}} subscritores", + "generic_subscriptions_count_0": "{{count}} subscrição", + "generic_subscriptions_count_1": "{{count}} subscrições", + "generic_subscriptions_count_2": "{{count}} subscrições", "comments_points_count_0": "{{count}} ponto", "comments_points_count_1": "{{count}} pontos", "comments_points_count_2": "{{count}} pontos", @@ -440,7 +440,7 @@ "crash_page_before_reporting": "Antes de reportar um erro, verifique se:", "crash_page_refresh": "tentou recarregar a página", "crash_page_switch_instance": "tentou usar outra instância", - "crash_page_read_the_faq": "leia as Perguntas frequentes (FAQ)", + "crash_page_read_the_faq": "leu as Perguntas frequentes (FAQ)", "crash_page_search_issue": "procurou se o erro já foi reportado no GitHub", "crash_page_report_issue": "Se nenhuma opção acima ajudou, por favor abra um novo problema no Github (preferencialmente em inglês) e inclua o seguinte texto (NÃO o traduza):", "user_created_playlists": "`x` listas de reprodução criadas", @@ -484,7 +484,7 @@ "channel_tab_playlists_label": "Listas de reprodução", "channel_tab_channels_label": "Canais", "channel_tab_shorts_label": "Curtos", - "channel_tab_streams_label": "Diretos", + "channel_tab_streams_label": "Emissões em direto", "Music in this video": "Música neste vídeo", "Artist: ": "Artista: ", "Album: ": "Álbum: ", @@ -493,17 +493,25 @@ "Standard YouTube license": "Licença padrão do YouTube", "Download is disabled": "A descarga está desativada", "Import YouTube playlist (.csv)": "Importar lista de reprodução do YouTube (.csv)", - "generic_button_delete": "Deletar", + "generic_button_delete": "Eliminar", "generic_button_edit": "Editar", "generic_button_rss": "RSS", "channel_tab_podcasts_label": "Podcasts", "channel_tab_releases_label": "Lançamentos", - "generic_button_save": "Salvar", + "generic_button_save": "Guardar", "generic_button_cancel": "Cancelar", "playlist_button_add_items": "Adicionar vídeos", "generic_channels_count_0": "{{count}} canal", "generic_channels_count_1": "{{count}} canais", "generic_channels_count_2": "{{count}} canais", "Import YouTube watch history (.json)": "Importar histórico de reprodução do YouTube (.json)", - "toggle_theme": "Trocar tema" + "toggle_theme": "Trocar tema", + "Add to playlist": "Adicionar à lista de reprodução", + "Add to playlist: ": "Adicionar à lista de reprodução: ", + "Answer": "Resposta", + "Search for videos": "Procurar vídeos", + "carousel_slide": "Diapositivo {{current}} de{{total}}", + "carousel_skip": "Ignorar carrossel", + "carousel_go_to": "Ir para o diapositivo`x`", + "The Popular feed has been disabled by the administrator.": "O feed Popular foi desativado por um administrador." } From 8d75d6431a399654e3588cfd0427b4c829f3d7f0 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 25 Apr 2024 18:35:08 +0200 Subject: [PATCH 574/598] Update Vietnamese translation Co-authored-by: Hosted Weblate Co-authored-by: Knight Hat --- locales/vi.json | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/locales/vi.json b/locales/vi.json index 4f8dc30d..229f8fa9 100644 --- a/locales/vi.json +++ b/locales/vi.json @@ -33,12 +33,12 @@ "Export data as JSON": "Xuất dữ liệu Invidious dưới dạng JSON", "Delete account?": "Xóa tài khoản?", "History": "Lịch sử", - "An alternative front-end to YouTube": "Một front-end thay thế cho YouTube", + "An alternative front-end to YouTube": "Giao diện thay thế cho YouTube", "JavaScript license information": "Thông tin giấy phép JavaScript", "source": "nguồn", "Log in": "Đăng nhập", "Log in/register": "Đăng nhập / đăng ký", - "User ID": "ID người dùng", + "User ID": "Mã nhận dạng người dùng", "Password": "Mật khẩu", "Time (h:mm:ss):": "Thời gian (h:mm:ss):", "Text CAPTCHA": "CAPTCHA dạng chữ", @@ -46,16 +46,16 @@ "Sign In": "Đăng nhập", "Register": "Đăng ký", "E-mail": "E-mail", - "Preferences": "Sở thích", + "Preferences": "Cài đặt", "preferences_category_player": "Tùy chọn trình phát video", "preferences_video_loop_label": "Luôn lặp lại: ", "preferences_autoplay_label": "Tự động phát: ", "preferences_continue_label": "Phát kế tiếp theo mặc định: ", "preferences_continue_autoplay_label": "Tự động phát video tiếp theo: ", "preferences_listen_label": "Nghe theo mặc định: ", - "preferences_local_label": "Video proxy: ", + "preferences_local_label": "Máy chủ sử lý video: ", "preferences_speed_label": "Tốc độ mặc định: ", - "preferences_quality_label": "Chất lượng video ưa thích: ", + "preferences_quality_label": "Chất lượng video: ", "preferences_volume_label": "Âm lượng video: ", "preferences_comments_label": "Nhận xét mặc định: ", "youtube": "YouTube", @@ -341,13 +341,13 @@ "([^.,0-9]|^)1([^.,0-9]|$)": "Hiển thị `x`bình luận" }, "Song: ": "Ca khúc: ", - "Premieres in `x`": "Trình chiếu lần đầu vào `x`", - "preferences_quality_dash_option_worst": "Thấp nhất", + "Premieres in `x`": "Trình chiếu ở `x`", + "preferences_quality_dash_option_worst": "Tệ nhất", "preferences_watch_history_label": "Bật lịch sử video đã xem ", "preferences_quality_option_hd720": "HD720", "unsubscribe": "hủy đăng kí", "revoke": "gỡ bỏ", - "preferences_quality_dash_label": "Chất lượng video DASH ưa thích ", + "preferences_quality_dash_label": "Chất lượng video DASH ", "preferences_quality_dash_option_auto": "Tự động", "Subscriptions": "Thuê bao", "View YouTube comments": "Hiển thị bình luận từ YouTube", @@ -470,5 +470,14 @@ "search_filters_duration_option_medium": "Trung bình (4 - 20 phút)", "generic_count_seconds_0": "{{count}} giây", "search_filters_date_label": "Ngày tải lên", - "crash_page_you_found_a_bug": "Có vẻ như bạn đã tìm ra lỗi trong Indivious!" + "crash_page_you_found_a_bug": "Có vẻ như bạn đã tìm ra lỗi trong Indivious!", + "Add to playlist": "Thêm vào danh sách phát", + "Add to playlist: ": "Thêm vào danh sách phát: ", + "Answer": "Trả lời", + "toggle_theme": "Bật/tắt diện mạo", + "carousel_slide": "Trang {{current}} trên tổng {{total}} trang", + "carousel_skip": "Bỏ qua Carousel", + "carousel_go_to": "Đi tới trang `x`", + "Search for videos": "Tìm kiếm video", + "The Popular feed has been disabled by the administrator.": "Bảng tin phổ biến đã bị tắt bởi ban quản lý." } From c8369f9dbb98515fcd119aaa27698dba8787340c Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 25 Apr 2024 18:35:08 +0200 Subject: [PATCH 575/598] Update Croatian translation Update Croatian translation Co-authored-by: Hosted Weblate Co-authored-by: Milo Ivir --- locales/hr.json | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/locales/hr.json b/locales/hr.json index 2d86144f..91425248 100644 --- a/locales/hr.json +++ b/locales/hr.json @@ -21,7 +21,7 @@ "Import and Export Data": "Uvezi i izvezi podatke", "Import": "Uvezi", "Import Invidious data": "Uvezi Invidious JSON podatke", - "Import YouTube subscriptions": "Uvezi YouTube/OPML pretplate", + "Import YouTube subscriptions": "Uvezi YouTube CSV ili OPML pretplate", "Import FreeTube subscriptions (.db)": "Uvezi FreeTube pretplate (.db)", "Import NewPipe subscriptions (.json)": "Uvezi NewPipe pretplate (.json)", "Import NewPipe data (.zip)": "Uvezi NewPipe podatke (.zip)", @@ -504,5 +504,14 @@ "generic_channels_count_0": "{{count}} kanal", "generic_channels_count_1": "{{count}} kanala", "generic_channels_count_2": "{{count}} kanala", - "Import YouTube watch history (.json)": "Uvezi YouTube povijest gledanja (.json)" + "Import YouTube watch history (.json)": "Uvezi YouTube povijest gledanja (.json)", + "Add to playlist": "Dodaj u zbirku", + "Add to playlist: ": "Dodaj u zbirku: ", + "Answer": "Odgovor", + "Search for videos": "Traži videa", + "The Popular feed has been disabled by the administrator.": "Popularni feed je administrator deaktivirao.", + "toggle_theme": "Uklj./Isklj. temu", + "carousel_slide": "Kadar {{current}} od {{total}}", + "carousel_go_to": "Idi na kadar `x`", + "carousel_skip": "Preskoči vrtuljak" } From ef7f3f5bd48a59f3cb63005d27faf52eb8b1abe6 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 25 Apr 2024 18:35:08 +0200 Subject: [PATCH 576/598] Update Hindi translation Update Hindi translation Co-authored-by: Hosted Weblate Co-authored-by: Scrambled777 --- locales/hi.json | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/locales/hi.json b/locales/hi.json index a7e0639a..0a1c09dd 100644 --- a/locales/hi.json +++ b/locales/hi.json @@ -62,7 +62,7 @@ "Import and Export Data": "डेटा को आयात और निर्यात करें", "Import": "आयात करें", "Import Invidious data": "Invidious JSON डेटा आयात करें", - "Import YouTube subscriptions": "YouTube/OPML सदस्यताएँ आयात करें", + "Import YouTube subscriptions": "YouTube CSV या OPML सदस्यताएँ आयात करें", "Import FreeTube subscriptions (.db)": "FreeTube सदस्यताएँ आयात करें (.db)", "Import NewPipe subscriptions (.json)": "NewPipe सदस्यताएँ आयात करें (.json)", "Import NewPipe data (.zip)": "NewPipe डेटा आयात करें (.zip)", @@ -487,5 +487,14 @@ "Download is disabled": "डाउनलोड करना अक्षम है", "generic_channels_count": "{{count}} चैनल", "generic_channels_count_plural": "{{count}} चैनल", - "Import YouTube watch history (.json)": "YouTube पर देखने का इतिहास आयात करें (.json)" + "Import YouTube watch history (.json)": "YouTube पर देखने का इतिहास आयात करें (.json)", + "Add to playlist": "प्लेलिस्ट में जोड़ें", + "Answer": "जवाब", + "The Popular feed has been disabled by the administrator.": "लोकप्रिय फ़ीड व्यवस्थापक द्वारा अक्षम कर दिया गया है।", + "toggle_theme": "थीम टॉगल करें", + "carousel_slide": "{{total}} में से स्लाइड {{current}}", + "carousel_skip": "कैरोसेल छोड़ें", + "Add to playlist: ": "प्लेलिस्ट में जोड़ें: ", + "Search for videos": "वीडियो खोजें", + "carousel_go_to": "स्लाइड `x` पर जाएँ" } From 5551b613d3b48cf8377c76ef81c7e80357173cdb Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 25 Apr 2024 18:35:08 +0200 Subject: [PATCH 577/598] Update Polish translation Update Polish translation Co-authored-by: Hosted Weblate Co-authored-by: Matthaiks --- locales/pl.json | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/locales/pl.json b/locales/pl.json index 0d18e90a..f24e9766 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -21,13 +21,13 @@ "Import and Export Data": "Import i eksport danych", "Import": "Import", "Import Invidious data": "Importuj dane JSON Invidious", - "Import YouTube subscriptions": "Importuj subskrybcje z YouTube/OPML", - "Import FreeTube subscriptions (.db)": "Importuj subskrybcje z FreeTube (.db)", - "Import NewPipe subscriptions (.json)": "Importuj subskrybcje z NewPipe (.json)", + "Import YouTube subscriptions": "Importuj subskrypcje YouTube w formacie CSV lub OPML", + "Import FreeTube subscriptions (.db)": "Importuj subskrypcje FreeTube (.db)", + "Import NewPipe subscriptions (.json)": "Importuj subskrypcje NewPipe (.json)", "Import NewPipe data (.zip)": "Importuj dane NewPipe (.zip)", "Export": "Eksport", - "Export subscriptions as OPML": "Eksportuj subskrybcje jako OPML", - "Export subscriptions as OPML (for NewPipe & FreeTube)": "Eksportuj subskrybcje jako OPML (dla NewPipe i FreeTube)", + "Export subscriptions as OPML": "Eksportuj subskrypcje jako OPML", + "Export subscriptions as OPML (for NewPipe & FreeTube)": "Eksportuj subskrypcje jako OPML (dla NewPipe i FreeTube)", "Export data as JSON": "Eksportuj dane Invidious jako JSON", "Delete account?": "Usunąć konto?", "History": "Historia", @@ -73,7 +73,7 @@ "preferences_thin_mode_label": "Tryb minimalny: ", "preferences_category_misc": "Różne preferencje", "preferences_automatic_instance_redirect_label": "Automatycznie przekierowanie instancji (powrót do redirect.invidious.io): ", - "preferences_category_subscription": "Preferencje subskrybcji", + "preferences_category_subscription": "Preferencje subskrypcji", "preferences_annotations_subscribed_label": "Domyślnie wyświetlaj adnotacje dla subskrybowanych kanałów: ", "Redirect homepage to feed: ": "Przekieruj stronę główną do subskrybcji: ", "preferences_max_results_label": "Liczba filmów widoczna na stronie subskrybcji: ", @@ -95,7 +95,7 @@ "Clear watch history": "Wyczyść historię", "Import/export data": "Import/Eksport danych", "Change password": "Zmień hasło", - "Manage subscriptions": "Organizuj subskrybcje", + "Manage subscriptions": "Organizuj subskrypcje", "Manage tokens": "Zarządzaj tokenami", "Watch history": "Historia", "Delete account": "Usuń konto", @@ -115,7 +115,7 @@ "Import/export": "Import/Eksport", "unsubscribe": "odsubskrybuj", "revoke": "cofnij", - "Subscriptions": "Subskrybcje", + "Subscriptions": "Subskrypcje", "search": "szukaj", "Log out": "Wyloguj", "Source available here.": "Kod źródłowy dostępny tutaj.", @@ -505,5 +505,13 @@ "generic_channels_count_1": "{{count}} kanały", "generic_channels_count_2": "{{count}} kanałów", "Import YouTube watch history (.json)": "Importuj historię oglądania z YouTube (.json)", - "toggle_theme": "Przełącz motyw" + "toggle_theme": "Przełącz motyw", + "The Popular feed has been disabled by the administrator.": "Kanał Popularne został wyłączony przez administratora.", + "Answer": "Odpowiedź", + "Search for videos": "Wyszukaj filmy", + "Add to playlist": "Dodaj do playlisty", + "Add to playlist: ": "Dodaj do playlisty: ", + "carousel_slide": "Slajd {{current}} z {{total}}", + "carousel_skip": "Pomiń karuzelę", + "carousel_go_to": "Przejdź do slajdu `x`" } From 0de3b0a96d0f2012c00938d1925625254b2c1137 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 25 Apr 2024 18:35:08 +0200 Subject: [PATCH 578/598] Update Italian translation Update Italian translation Co-authored-by: Federico Co-authored-by: Hosted Weblate --- locales/it.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/locales/it.json b/locales/it.json index 7b6bb5d9..79aa6c16 100644 --- a/locales/it.json +++ b/locales/it.json @@ -504,5 +504,14 @@ "generic_channels_count_0": "{{count}} canale", "generic_channels_count_1": "{{count}} canali", "generic_channels_count_2": "{{count}} canali", - "Import YouTube watch history (.json)": "Importa la cronologia delle visualizzazioni di YouTube (.json)" + "Import YouTube watch history (.json)": "Importa la cronologia delle visualizzazioni di YouTube (.json)", + "Answer": "Risposta", + "toggle_theme": "Cambia Tema", + "Add to playlist": "Aggiungi alla playlist", + "Add to playlist: ": "Aggiungi alla playlist ", + "Search for videos": "Cerca dei video", + "The Popular feed has been disabled by the administrator.": "La sezione dei contenuti popolari è stata disabilitata dall'amministratore.", + "carousel_slide": "Fotogramma {{current}} di {{total}}", + "carousel_skip": "Salta la galleria", + "carousel_go_to": "Vai al fotogramma `x`" } From c60d2561d1de726000226bdc2ae2baf90ecd743c Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 25 Apr 2024 18:35:08 +0200 Subject: [PATCH 579/598] Update Arabic translation Update Arabic translation Update Arabic translation Update Arabic translation Co-authored-by: Hosted Weblate Co-authored-by: Rex_sa Co-authored-by: Samantaz Fox --- locales/ar.json | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/locales/ar.json b/locales/ar.json index 57062e89..5d8b230f 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -15,13 +15,13 @@ "New password": "كلمة مرور جديدة", "New passwords must match": "يَجبُ أن تكون كلمتا المرور متطابقتين", "Authorize token?": "رمز التفويض؟", - "Authorize token for `x`?": "السماح بالرمز المميز ل 'x'؟", + "Authorize token for `x`?": "السماح بالرمز المميز ل `x`؟", "Yes": "نعم", "No": "لا", "Import and Export Data": "اِستيراد البيانات وتصديرها", "Import": "استيراد", "Import Invidious data": "استيراد بيانات JSON Invidious", - "Import YouTube subscriptions": "استيراد اشتراكات YouTube/OPML", + "Import YouTube subscriptions": "استيراد الاشتراكات YouTube بتنسيق CSV أو OPML", "Import FreeTube subscriptions (.db)": "استيراد اشتراكات فريتيوب (.db)", "Import NewPipe subscriptions (.json)": "استيراد اشتراكات نيو بايب (.json)", "Import NewPipe data (.zip)": "استيراد بيانات نيو بايب (.zip)", @@ -170,7 +170,7 @@ "Password cannot be empty": "لا يمكن أن تكون كلمة السر فارغة", "Password cannot be longer than 55 characters": "يجب أن لا تتعدى كلمة السر 55 حرفًا", "Please log in": "الرجاء تسجيل الدخول", - "Invidious Private Feed for `x`": "تغذية Invidious خاصة ل 'x'", + "Invidious Private Feed for `x`": "تغذية Invidious خاصة ل `x`", "channel:`x`": "قناة:`x`", "Deleted or invalid channel": "قناة ممسوحة او غير صالحة", "This channel does not exist.": "هذه القناة غير موجودة.", @@ -382,11 +382,11 @@ "videoinfo_watch_on_youTube": "مشاهدة على يوتيوب", "videoinfo_youTube_embed_link": "مضمن", "videoinfo_invidious_embed_link": "رابط مضمن", - "user_created_playlists": "'x' إنشاء قوائم التشغيل", - "user_saved_playlists": "قوائم التشغيل المحفوظة 'x'", + "user_created_playlists": "`x` إنشاء قوائم التشغيل", + "user_saved_playlists": "قوائم التشغيل المحفوظة `x`", "Video unavailable": "الفيديو غير متوفر", "search_filters_features_option_three_sixty": "360°", - "download_subtitles": "ترجمات - 'x' (.vtt)", + "download_subtitles": "ترجمات - `x` (.vtt)", "invidious": "الخيالي", "preferences_save_player_pos_label": "حفظ موضع التشغيل: ", "crash_page_you_found_a_bug": "يبدو أنك قد وجدت خطأً برمجيًّا في Invidious!", @@ -556,5 +556,13 @@ "generic_channels_count_4": "{{count}} قنوات", "generic_channels_count_5": "{{count}} قناة", "Import YouTube watch history (.json)": "استيراد سجل مشاهدة YouTube بصيغة (.json)", - "toggle_theme": "تبديل الموضوع" + "toggle_theme": "تبديل الموضوع", + "Add to playlist": "أضف إلى قائمة التشغيل", + "Add to playlist: ": "أضف إلى قائمة التشغيل: ", + "Answer": "الرد", + "Search for videos": "ابحث عن مقاطع الفيديو", + "The Popular feed has been disabled by the administrator.": "تم تعطيل الخلاصة الشائعة من قبل المسؤول.", + "carousel_slide": "الشريحة {{current}} من {{total}}", + "carousel_skip": "تخطي الكاروسيل", + "carousel_go_to": "انتقل إلى الشريحة `x`" } From 3f9c7b6c19d365628c72f8b36589765ff4fdc764 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 25 Apr 2024 18:35:08 +0200 Subject: [PATCH 580/598] Update Interlingua translation Co-authored-by: Hosted Weblate Co-authored-by: Software In Interlingua --- locales/ia.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/locales/ia.json b/locales/ia.json index 19b6b0c0..2c8cb2b0 100644 --- a/locales/ia.json +++ b/locales/ia.json @@ -37,5 +37,9 @@ "E-mail": "E-mail", "Delete account?": "Deler conto?", "preferences_volume_label": "Volumine del reproductor: ", - "preferences_sort_label": "Ordinar le videos per: " + "preferences_sort_label": "Ordinar le videos per: ", + "Next page": "Pagina sequente", + "Previous page": "Pagina previe", + "Yes": "Si", + "Import": "Importar" } From 64eef948bded9d5fd2a2ee29b800d368ba3587eb Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 25 Apr 2024 18:35:08 +0200 Subject: [PATCH 581/598] Update Dutch translation Co-authored-by: Gert-dev Co-authored-by: Hosted Weblate --- locales/nl.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/locales/nl.json b/locales/nl.json index a30bc5b5..d495a2d1 100644 --- a/locales/nl.json +++ b/locales/nl.json @@ -487,5 +487,14 @@ "generic_button_delete": "Verwijderen", "Import YouTube playlist (.csv)": "YouTube-afspeellijst importeren (.csv)", "Standard YouTube license": "Standaard YouTube-licentie", - "Import YouTube watch history (.json)": "YouTube-kijkgeschiedenis importeren (.json)" + "Import YouTube watch history (.json)": "YouTube-kijkgeschiedenis importeren (.json)", + "Add to playlist": "Aan afspeellijst toevoegen", + "The Popular feed has been disabled by the administrator.": "De Populaire feed werd uitgeschakeld door een beheerder.", + "carousel_slide": "Dia {{current}} van {{total}}", + "carousel_go_to": "Naar dia `x` gaan", + "Add to playlist: ": "Aan afspeellijst toevoegen: ", + "Answer": "Antwoorden", + "Search for videos": "Naar video's zoeken", + "carousel_skip": "Carousel overslaan", + "toggle_theme": "Thema omschakelen" } From b54d45504ffc2ad10f59a4ad540444daa781dff6 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 25 Apr 2024 18:35:08 +0200 Subject: [PATCH 582/598] Update Spanish translation Update Spanish translation Update Spanish translation Co-authored-by: Hosted Weblate Co-authored-by: Samantaz Fox Co-authored-by: gallegonovato --- locales/es.json | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/locales/es.json b/locales/es.json index 7a41710e..1d082e60 100644 --- a/locales/es.json +++ b/locales/es.json @@ -21,7 +21,7 @@ "Import and Export Data": "Importación y exportación de datos", "Import": "Importar", "Import Invidious data": "Importar datos JSON de Invidious", - "Import YouTube subscriptions": "Importar suscripciones de YouTube/OPML", + "Import YouTube subscriptions": "Importar suscripciones CSV u OPML de YouTube", "Import FreeTube subscriptions (.db)": "Importar suscripciones de FreeTube (.db)", "Import NewPipe subscriptions (.json)": "Importar suscripciones de NewPipe (.json)", "Import NewPipe data (.zip)": "Importar datos de NewPipe (.zip)", @@ -133,7 +133,7 @@ "Create playlist": "Crear lista de reproducción", "Title": "Título", "Playlist privacy": "Privacidad de la lista de reproducción", - "Editing playlist `x`": "Editando la lista de reproducción 'x'", + "Editing playlist `x`": "Editando la lista de reproducción `x`", "Show more": "Mostrar más", "Show less": "Mostrar menos", "Watch on YouTube": "Ver en YouTube", @@ -505,5 +505,13 @@ "generic_channels_count_1": "{{count}} canales", "generic_channels_count_2": "{{count}} canales", "Import YouTube watch history (.json)": "Importar el historial de las visualizaciones de YouTube (.json)", - "toggle_theme": "Alternar tema" + "toggle_theme": "Alternar tema", + "Add to playlist: ": "Añadir a la lista de reproducción: ", + "Add to playlist": "Añadir a la lista de reproducción", + "Answer": "Respuesta", + "Search for videos": "Buscar por vídeos", + "The Popular feed has been disabled by the administrator.": "El feed Popular ha sido desactivado por el administrador.", + "carousel_slide": "Diapositiva {{current}} de {{total}}", + "carousel_skip": "Saltar el carrusel", + "carousel_go_to": "Ir a la diapositiva `x`" } From e3018e00c4d745563834f4ca803e8897f129254b Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 25 Apr 2024 18:35:08 +0200 Subject: [PATCH 583/598] Update Swedish translation Co-authored-by: Hosted Weblate Co-authored-by: bittin1ddc447d824349b2 --- locales/sv-SE.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/locales/sv-SE.json b/locales/sv-SE.json index db3486df..76edc341 100644 --- a/locales/sv-SE.json +++ b/locales/sv-SE.json @@ -488,5 +488,13 @@ "crash_page_you_found_a_bug": "Det verkar som att du har hittat en bugg i Invidious!", "generic_views_count": "{{count}} visning", "generic_views_count_plural": "{{count}} visningar", - "toggle_theme": "Växla tema" + "toggle_theme": "Växla tema", + "Add to playlist": "Lägg till i spellista", + "Add to playlist: ": "Lägg till i spellista: ", + "Answer": "Svara", + "Search for videos": "Sök efter videor", + "The Popular feed has been disabled by the administrator.": "Det populära flödet har inaktiverats av administratören.", + "carousel_slide": "Bildspel {{current}} av {{total}}", + "carousel_skip": "Hoppa över karusellen", + "carousel_go_to": "Gå till bildspel `x`" } From eba0699c481130090d9a718cffe9e1576f36bdf6 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 25 Apr 2024 18:35:08 +0200 Subject: [PATCH 584/598] Update Serbian translation Update Serbian translation Co-authored-by: Hosted Weblate Co-authored-by: NEXI --- locales/sr.json | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/locales/sr.json b/locales/sr.json index b4a98da6..4b24e7c0 100644 --- a/locales/sr.json +++ b/locales/sr.json @@ -21,7 +21,7 @@ "Import and Export Data": "Uvoz i izvoz podataka", "Import": "Uvezi", "Import Invidious data": "Uvezi Invidious JSON podatke", - "Import YouTube subscriptions": "Uvezi YouTube/OPML praćenja", + "Import YouTube subscriptions": "Uvezi YouTube CSV ili OPML praćenja", "Import FreeTube subscriptions (.db)": "Uvezi FreeTube praćenja (.db)", "Import NewPipe subscriptions (.json)": "Uvezi NewPipe praćenja (.json)", "Import NewPipe data (.zip)": "Uvezi NewPipe podatke (.zip)", @@ -504,5 +504,14 @@ "generic_views_count_0": "{{count}} pregled", "generic_views_count_1": "{{count}} pregleda", "generic_views_count_2": "{{count}} pregleda", - "Import YouTube watch history (.json)": "Uvezi YouTube istoriju gledanja (.json)" + "Import YouTube watch history (.json)": "Uvezi YouTube istoriju gledanja (.json)", + "The Popular feed has been disabled by the administrator.": "Administrator je onemogućio fid „Popularno“.", + "Add to playlist: ": "Dodajte na plejlistu: ", + "Add to playlist": "Dodaj na plejlistu", + "carousel_slide": "Slajd {{current}} od {{total}}", + "carousel_go_to": "Idi na slajd `x`", + "Answer": "Odgovor", + "Search for videos": "Pretražite video snimke", + "carousel_skip": "Preskoči karusel", + "toggle_theme": "Подеси тему" } From 58dc63671aaf16eb17e958542fb10c6aa6cb4dce Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 25 Apr 2024 18:35:08 +0200 Subject: [PATCH 585/598] Update Korean translation Update Korean translation Update Korean translation Co-authored-by: Hosted Weblate Co-authored-by: simmon Co-authored-by: xrfmkrh --- locales/ko.json | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/locales/ko.json b/locales/ko.json index c0257ee5..7611e8e7 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -460,7 +460,7 @@ "Music in this video": "동영상 속 음악", "Artist: ": "아티스트: ", "Download is disabled": "다운로드가 비활성화 되어있음", - "Import YouTube playlist (.csv)": "유튜브 플레이리스트 가져오기 (.csv)", + "Import YouTube playlist (.csv)": "유튜브 재생목록 가져오기 (.csv)", "playlist_button_add_items": "동영상 추가", "channel_tab_podcasts_label": "팟캐스트", "generic_button_delete": "삭제", @@ -468,7 +468,16 @@ "generic_button_save": "저장", "generic_button_cancel": "취소", "generic_button_rss": "RSS", - "channel_tab_releases_label": "출시", + "channel_tab_releases_label": "발매", "generic_channels_count_0": "{{count}} 채널", - "Import YouTube watch history (.json)": "유튜브 시청 기록 가져오기 (.json)" + "Import YouTube watch history (.json)": "유튜브 시청 기록 가져오기 (.json)", + "Add to playlist": "재생목록에 추가", + "Add to playlist: ": "재생목록에 추가: ", + "Answer": "답", + "The Popular feed has been disabled by the administrator.": "관리자가 인기 피드를 비활성화했습니다.", + "carousel_skip": "캐러셀 건너뛰기", + "carousel_go_to": "`x` 슬라이드로 이동", + "Search for videos": "비디오 검색", + "toggle_theme": "테마 전환", + "carousel_slide": "{{total}}의 슬라이드 {{current}}" } From 6ed872d72b84851fd97cf3e13b22cb85fc6bd773 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 25 Apr 2024 18:35:09 +0200 Subject: [PATCH 586/598] Update English (United States) translation Co-authored-by: Hosted Weblate Co-authored-by: Lime bar --- locales/en-US.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/en-US.json b/locales/en-US.json index 10887612..3987f796 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -43,7 +43,7 @@ "Import and Export Data": "Import and Export Data", "Import": "Import", "Import Invidious data": "Import Invidious JSON data", - "Import YouTube subscriptions": "Import YouTube/OPML subscriptions", + "Import YouTube subscriptions": "Import YouTube CSV or OPML subscriptions", "Import YouTube playlist (.csv)": "Import YouTube playlist (.csv)", "Import YouTube watch history (.json)": "Import YouTube watch history (.json)", "Import FreeTube subscriptions (.db)": "Import FreeTube subscriptions (.db)", From 200cfd7579c1abea4524dda419e357407a7e1fe4 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 25 Apr 2024 18:35:09 +0200 Subject: [PATCH 587/598] Update Portuguese (Portugal) translation Co-authored-by: Samantaz Fox --- locales/pt-PT.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/locales/pt-PT.json b/locales/pt-PT.json index 3834c9e2..f83a80a9 100644 --- a/locales/pt-PT.json +++ b/locales/pt-PT.json @@ -130,12 +130,12 @@ "Private": "Privado", "View all playlists": "Ver todas as listas de reprodução", "Updated `x` ago": "Atualizado `x` atrás", - "Delete playlist `x`?": "Eliminar a lista de reprodução 'x'?", + "Delete playlist `x`?": "Eliminar a lista de reprodução `x`?", "Delete playlist": "Eliminar lista de reprodução", "Create playlist": "Criar lista de reprodução", "Title": "Título", "Playlist privacy": "Privacidade da lista de reprodução", - "Editing playlist `x`": "A editar lista de reprodução 'x'", + "Editing playlist `x`": "A editar lista de reprodução `x`", "Show more": "Mostrar mais", "Show less": "Mostrar menos", "Watch on YouTube": "Ver no YouTube", @@ -150,8 +150,8 @@ "Whitelisted regions: ": "Regiões permitidas: ", "Blacklisted regions: ": "Regiões bloqueadas: ", "Shared `x`": "Partilhado `x`", - "Premieres in `x`": "Estreias em 'x'", - "Premieres `x`": "Estreias 'x'", + "Premieres in `x`": "Estreias em `x`", + "Premieres `x`": "Estreias `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.": "Olá! Parece que o JavaScript está desativado. Clique aqui para ver os comentários, entretanto eles podem levar mais tempo para carregar.", "View YouTube comments": "Ver comentários do YouTube", "View more comments on Reddit": "Ver mais comentários no Reddit", @@ -173,7 +173,7 @@ "Password cannot be longer than 55 characters": "A palavra-chave não pode ser superior a 55 caracteres", "Please log in": "Por favor, inicie sessão", "Invidious Private Feed for `x`": "Feed Privado do Invidious para `x`", - "channel:`x`": "canal:'x'", + "channel:`x`": "canal:`x`", "Deleted or invalid channel": "Canal eliminado ou inválido", "This channel does not exist.": "Este canal não existe.", "Could not get channel info.": "Não foi possível obter as informações do canal.", From 7546cb511d0a2c52250c4b3e573af0f559b7d9cc Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 25 Apr 2024 18:35:09 +0200 Subject: [PATCH 588/598] Update Chinese (Traditional) translation Update Chinese (Traditional) translation Co-authored-by: Hosted Weblate Co-authored-by: Jeff Huang --- locales/zh-TW.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 1520c269..2584db9c 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -26,7 +26,7 @@ "Import and Export Data": "匯入與匯出資料", "Import": "匯入", "Import Invidious data": "匯入 Invidious JSON 資料", - "Import YouTube subscriptions": "匯入 YouTube/OPML 訂閱", + "Import YouTube subscriptions": "匯入 YouTube CSV 或 OPML 訂閱", "Import FreeTube subscriptions (.db)": "匯入 FreeTube 訂閱 (.db)", "Import NewPipe subscriptions (.json)": "匯入 NewPipe 訂閱 (.json)", "Import NewPipe data (.zip)": "匯入 NewPipe 資料 (.zip)", @@ -471,5 +471,13 @@ "channel_tab_podcasts_label": "Podcast", "channel_tab_releases_label": "發布", "generic_channels_count_0": "{{count}} 個頻道", - "toggle_theme": "切換佈景主題" + "toggle_theme": "切換佈景主題", + "Add to playlist": "新增至播放清單", + "Add to playlist: ": "新增至播放清單: ", + "Answer": "答案", + "Search for videos": "搜尋影片", + "carousel_slide": "第 {{current}} 張投影片,共 {{total}} 張", + "carousel_skip": "略過輪播", + "carousel_go_to": "跳到投影片 `x`", + "The Popular feed has been disabled by the administrator.": "熱門 feed 已被管理員停用。" } From 2da63bf36dce0b63f0043f13177480fb8b383a1c Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 25 Apr 2024 18:35:09 +0200 Subject: [PATCH 589/598] Update Chinese (Simplified) translation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update Chinese (Simplified) translation Co-authored-by: Hosted Weblate Co-authored-by: 大王叫我来巡山 --- locales/zh-CN.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/locales/zh-CN.json b/locales/zh-CN.json index faa67e6c..756645f4 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -26,7 +26,7 @@ "Import and Export Data": "导入与导出数据", "Import": "导入", "Import Invidious data": "导入 Invidious JSON 数据", - "Import YouTube subscriptions": "导入 YouTube/OPML 订阅", + "Import YouTube subscriptions": "导入 YouTube CSV 或 OPML 订阅", "Import FreeTube subscriptions (.db)": "导入 FreeTube 订阅 (.db)", "Import NewPipe subscriptions (.json)": "导入 NewPipe 订阅 (.json)", "Import NewPipe data (.zip)": "导入 NewPipe 数据 (.zip)", @@ -471,5 +471,13 @@ "generic_button_rss": "RSS", "channel_tab_releases_label": "公告", "generic_channels_count_0": "{{count}} 个频道", - "toggle_theme": "切换主题" + "toggle_theme": "切换主题", + "Add to playlist": "添加到播放列表", + "Add to playlist: ": "添加到播放列表: ", + "Answer": "响应", + "Search for videos": "搜索视频", + "The Popular feed has been disabled by the administrator.": "“流行”源已被管理员禁用。", + "carousel_slide": "当前为第 {{current}} 张图,共 {{total}} 张图", + "carousel_skip": "跳过图集", + "carousel_go_to": "转到图 `x`" } From bff0b5c85a7061a41b795687f90ebf019b3129a7 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 25 Apr 2024 18:35:09 +0200 Subject: [PATCH 590/598] Update Serbian (cyrillic) translation Update Serbian (cyrillic) translation Co-authored-by: Hosted Weblate Co-authored-by: NEXI --- locales/sr_Cyrl.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/locales/sr_Cyrl.json b/locales/sr_Cyrl.json index 52ac4116..57c6de9c 100644 --- a/locales/sr_Cyrl.json +++ b/locales/sr_Cyrl.json @@ -21,7 +21,7 @@ "Import and Export Data": "Увоз и извоз података", "Import": "Увези", "Import Invidious data": "Увези Invidious JSON податке", - "Import YouTube subscriptions": "Увези YouTube/OPML праћења", + "Import YouTube subscriptions": "Увези YouTube CSV или OPML праћења", "Import FreeTube subscriptions (.db)": "Увези FreeTube праћења (.db)", "Import NewPipe subscriptions (.json)": "Увези NewPipe праћења (.json)", "Import NewPipe data (.zip)": "Увези NewPipe податке (.zip)", @@ -505,5 +505,13 @@ "generic_views_count_1": "{{count}} прегледа", "generic_views_count_2": "{{count}} прегледа", "Import YouTube watch history (.json)": "Увези YouTube историју гледањa (.json)", - "toggle_theme": "Укључи тему" + "toggle_theme": "Укључи тему", + "Add to playlist": "Додај на плејлисту", + "Answer": "Одговор", + "Search for videos": "Претражите видео снимке", + "carousel_go_to": "Иди на слајд `x`", + "Add to playlist: ": "Додајте на плејлисту: ", + "carousel_skip": "Прескочи карусел", + "The Popular feed has been disabled by the administrator.": "Администратор је онемогућио фид „Популарно“.", + "carousel_slide": "Слајд {{current}} од {{total}}" } From 01e2a5e89d48823ca8eeb323643697b73187b8d5 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 25 Apr 2024 18:35:09 +0200 Subject: [PATCH 591/598] Update Lombard translation Update translation files Updated by "Remove blank strings" hook in Weblate. Update Lombard translation Add Lombard translation Co-authored-by: Federico Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/invidious/translations/ Translation: Invidious/Invidious Translations --- locales/lmo.json | 232 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 locales/lmo.json diff --git a/locales/lmo.json b/locales/lmo.json new file mode 100644 index 00000000..9d2fe2a8 --- /dev/null +++ b/locales/lmo.json @@ -0,0 +1,232 @@ +{ + "Add to playlist": "Giont a la playlist", + "generic_button_edit": "Modifega", + "generic_button_save": "Salva", + "LIVE": "EN DÌRETT", + "Shared `x` ago": "Compartiss `x` fa", + "View channel on YouTube": "Varda el canal sul YouTube", + "newest": "plù nöeuf", + "oldest": "plù végh", + "Search for videos": "Càuta dei video", + "The Popular feed has been disabled by the administrator.": "la seziùn Pupular la è stada disabilidada par l'amministratòr.", + "generic_channels_count": "{{count}} canal", + "generic_channels_count_plural": "{{count}} canai", + "popular": "pupular", + "generic_views_count": "{{count}} visualisazión", + "generic_views_count_plural": "{{count}} visualisazióni", + "generic_videos_count": "{{count}} video", + "generic_videos_count_plural": "{{count}} video", + "generic_playlists_count": "{{count}} playlist", + "generic_playlists_count_plural": "{{count}} playlist", + "generic_subscriptions_count": "{{count}} inscrizion", + "generic_subscriptions_count_plural": "{{count}} inscrizioni", + "generic_button_cancel": "Cançéla", + "generic_button_delete": "Scassa via", + "Unsubscribe": "Disinscriviti", + "Next page": "Pagina siguènt", + "Previous page": "Pagina indrèe", + "Clear watch history?": "Cançélar la istoria dei video vardàa?", + "New password": "Nöeva password", + "Import and Export Data": "Importazion ed esportazion dei dat", + "Import": "Importa", + "Import Invidious data": "Importa i dat de l'Invidious en el formàt JSON", + "Import YouTube subscriptions": "Importa le inscrizioni dal YouTube/OPML", + "Import YouTube playlist (.csv)": "Importa le playlist dal YouTube (.csv)", + "Import YouTube watch history (.json)": "Importa la istoria de visualizazzion dal YouTube (.json)", + "Import FreeTube subscriptions (.db)": "Importa le inscrizioni dal FreeTube (.db)", + "Import NewPipe data (.zip)": "importa i dat del NewPipe (.zip)", + "Export": "Esporta", + "Export subscriptions as OPML": "Esporta inscrizioni com OPML", + "Export data as JSON": "Esporta i dat de l'Invidious com JSON", + "Delete account?": "Eliminà 'l profil?", + "History": "Istoria", + "An alternative front-end to YouTube": "Una interfacia alternatif al YouTube", + "JavaScript license information": "Informaziòn su la licensa JavaScript", + "source": "font", + "Log in": "Và dent", + "Text CAPTCHA": "Tèst del CAPTCHA", + "Image CAPTCHA": "Imàgen del CAPTCHA", + "Sign In": "Ven denter", + "Register": "Registres", + "E-mail": "E-mail", + "Preferences": "Priferenze", + "preferences_category_player": "Priferenze del riprodutòr", + "preferences_quality_option_dash": "DASH (qualità adatif)", + "preferences_quality_option_hd720": "HD720", + "preferences_quality_option_medium": "Media", + "preferences_quality_option_small": "Picinina", + "preferences_quality_dash_option_auto": "Auto", + "preferences_quality_dash_option_best": "Meglior", + "preferences_quality_dash_option_worst": "Peggior", + "preferences_quality_dash_option_4320p": "4320p", + "preferences_quality_dash_option_2160p": "2160p", + "preferences_quality_dash_option_1440p": "1440p", + "preferences_quality_dash_option_1080p": "1080p", + "preferences_quality_dash_option_720p": "720p", + "preferences_quality_dash_option_480p": "480p", + "preferences_quality_dash_option_360p": "360p", + "preferences_quality_dash_option_240p": "240p", + "preferences_quality_dash_option_144p": "144p", + "reddit": "Reddit", + "invidious": "Invidious", + "light": "ciar", + "dark": "scur", + "preferences_category_misc": "Priferenze varie", + "preferences_category_subscription": "Priferenze de le inscrizioni", + "published": "data de publicazion", + "published - reverse": "data de publicazion - invertì", + "alphabetically": "orden alfabetegh", + "channel name": "nòm del canal", + "channel name - reverse": "nòm del canal - invertì", + "Enable web notifications": "Empisa le notifeghe da la red", + "`x` uploaded a video": "`x` la ghàa cargà un video", + "`x` is live": "`x` l'è 'n dirétt adés", + "preferences_category_data": "Priferenze dei dat", + "Import/export data": "Importa/esporta i dat", + "Change password": "Cambia la parola ciav", + "Manage subscriptions": "Organisa le inscrizioni", + "Manage tokens": "Organisa i tokens", + "Watch history": "Istoria dei video vardà", + "Delete account": "Cançéla 'l profil", + "Save preferences": "Salva priferenze", + "Subscription manager": "Manegia le inscrizioni", + "Token": "Token", + "tokens_count": "{{count}} token", + "tokens_count_plural": "{{count}} token", + "Import/export": "Importa/esporta", + "unsubscribe": "disinscriviti", + "subscriptions_unseen_notifs_count": "{{count}} notifega mia visualisada", + "subscriptions_unseen_notifs_count_plural": "{{count}} notifeghe mia visualisade", + "Log out": "Sortiss", + "Released under the AGPLv3 on Github.": "Publicà en el GitHub suta licenza AGPLv3.", + "Source available here.": "Codegh de la font disponivel chì.", + "View privacy policy.": "Varda la pulitega de la privacy.", + "Trending": "De moda", + "Public": "Publico", + "Unlisted": "Non en lista", + "Private": "Privàt", + "View all playlists": "Varda tute le playlist", + "Updated `x` ago": "Giurnà `x` fa", + "Delete playlist `x`?": "Cançéla la playlist `x`?", + "Delete playlist": "Cançéla playlist", + "Create playlist": "Crea playlist", + "Title": "Titel", + "Playlist privacy": "Privacy de la playlist", + "Editing playlist `x`": "Modifega playlist `x`", + "playlist_button_add_items": "Gionta video", + "Show more": "Varda plù", + "Show less": "Varda mèn", + "Watch on YouTube": "Varda sul YouTube", + "Switch Invidious Instance": "Cambia la instanza del Invidious", + "search_message_no_results": "Non è stat truvà nigun resultat.", + "Cebuano": "Cebuano", + "Chinese (Traditional)": "Cines (Tradizional)", + "Corsican": "Còrso", + "Croatian": "Cruat", + "Georgian": "Georgian", + "Gujarati": "Gujarati", + "Hawaiian": "Hawaiian", + "Kurdish": "Curd", + "Latin": "Latin", + "Latvian": "Letton", + "Lithuanian": "Lituan", + "Malay": "Males", + "Maltese": "Maltes", + "Mongolian": "móngol", + "Persian": "Persian", + "Polish": "Polacch", + "Portuguese": "Portoghes", + "Romanian": "Romen", + "Scottish Gaelic": "Gaelich Scusses", + "Spanish (Latin America)": "Spagnöl (America do Sùd)", + "Thai": "Thai", + "Western Frisian": "Frisian do ponente", + "Basque": "Basco", + "Chinese (Simplified)": "Cines (Semplificà)", + "Haitian Creole": "Creolo de Haiti", + "Galician": "Galiçian", + "Hebrew": "Ebraich", + "Korean": "Corean", + "View playlist on YouTube": "Varda la playlist sul YouTube", + "Southern Sotho": "Sotho do Sùd", + "generic_button_rss": "RSS", + "Welsh": "Galés", + "Answer": "Resposta", + "New passwords must match": "Le nöeve password la deven esere uguai", + "Authorize token?": "Autorisà 'l token?", + "Authorize token for `x`?": "Autorisà 'l token par `x`?", + "Yes": "Sì", + "No": "No", + "Export subscriptions as OPML (for NewPipe & FreeTube)": "Esporta inscrizioni com OPML (par 'l NewPipe e 'l FreeTube)", + "Log in/register": "Va dent/Registres", + "User ID": "ID utent", + "Password": "Parola ciav", + "Time (h:mm:ss):": "Temp (h:mm:ss):", + "Import NewPipe subscriptions (.json)": "importa le inscrizioni dal NewPipe (.json)", + "youtube": "YouTube", + "alphabetically - reverse": "orden alfabetegh - invertì", + "preferences_category_visual": "Priferenze grafeghe", + "Clear watch history": "Scompartiss la istoria dei video vardà", + "preferences_category_admin": "Priferenze de l'amministratòr", + "Token manager": "Manegia i token", + "Subscriptions": "Inscrizioni", + "search": "cerca", + "View JavaScript license information.": "Varda le informazion su la licenza JavaScript.", + "search_message_change_filters_or_query": "Ti pödi pruà a slargà la reçerca e/or a cangià i filter.", + "generic_subscribers_count": "{{count}} inscritt", + "generic_subscribers_count_plural": "{{count}} inscriti", + "Subscribe": "Inscriviti", + "last": "ùltim", + "Add to playlist: ": "Giont a la playlist: ", + "preferences_autoplay_label": "Reproduzion automatega: ", + "preferences_continue_label": "Reproduzion seguént preimpostà: ", + "preferences_continue_autoplay_label": "Fa partì en automatico el video seguént: ", + "preferences_listen_label": "Modalità de sól audio preimpostà: ", + "preferences_local_label": "Proxy par i video: ", + "preferences_watch_history_label": "Ativà la istoria de reproduzion: ", + "preferences_speed_label": "Velocità preimpostà: ", + "preferences_volume_label": "Volume del reprodutòr: ", + "preferences_region_label": "Nazion del contenut: ", + "Dark mode: ": "Tema scur ", + "preferences_dark_mode_label": "Tema: ", + "preferences_thin_mode_label": "Modalità legera: ", + "preferences_automatic_instance_redirect_label": "Reindirizazzion automatega de la instansa (rivèrt a redirect.invidious.io): ", + "Hide annotations": "Piaca le notazioni", + "Show annotations": "Mostra le notazioni", + "Family friendly? ": "Adàt a tüti? ", + "Whitelisted regions: ": "Regioni en lista bianca: ", + "Blacklisted regions: ": "Regioni en lista negher ", + "Artist: ": "Artista: ", + "Song: ": "Cansòn ", + "Album: ": "Album: ", + "View YouTube comments": "Varda i comment dal YouTube", + "Password cannot be empty": "La parola ciav la no po miga esser voeut", + "channel:`x`": "Canal:`x`", + "Bangla": "Bengales", + "Hausa": "Hausa", + "Hindi": "Hindi", + "Hmong": "Hmong", + "Igbo": "Igbo", + "Javanese": "Javanese", + "Kannada": "Kannada", + "Kazakh": "Kazach", + "Khmer": "Khmer", + "Kyrgyz": "Kirghiz", + "Lao": "Lao", + "Luxembourgish": "Lussemburghes", + "Macedonian": "Macedon", + "Malagasy": "Malagascio", + "Malayalam": "Malayalam", + "Maori": "Maori", + "Marathi": "Marati", + "Nepali": "Nepales", + "Nyanja": "Nyanja", + "Pashto": "Pashtu", + "Punjabi": "Punjabi", + "Samoan": "Samoan", + "Standard YouTube license": "licensa predefinida de Youtube", + "License: ": "Licensa: ", + "Music in this video": "Musica en sto video", + "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Ué! Sembra che ti la g'hà desabilitàa el JavaScript. Schisa chì para vardà i comment, ma cunsidera che peul vörse 'n po plu de temp a cargà.", + "preferences_video_loop_label": "Reproduci sèmper: " +} From 7f3ddad12edc409b3b39fc47e66c5439165fcaa8 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Fri, 26 Apr 2024 22:03:59 +0200 Subject: [PATCH 592/598] Videos: Use android test suite client --- src/invidious/videos/parser.cr | 8 +++----- src/invidious/yt_backend/youtube_api.cr | 15 +++++++++++---- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr index 75fe4a36..373f7227 100644 --- a/src/invidious/videos/parser.cr +++ b/src/invidious/videos/parser.cr @@ -107,7 +107,7 @@ def extract_video_info(video_id : String, proxy_region : String? = nil) # decrypted URLs and maybe fix throttling issues (#2194). See the # following issue for an explanation about decrypted URLs: # https://github.com/TeamNewPipe/NewPipeExtractor/issues/562 - client_config.client_type = YoutubeAPI::ClientType::Android + client_config.client_type = YoutubeAPI::ClientType::AndroidTestSuite new_player_response = try_fetch_streaming_data(video_id, client_config) elsif !reason.includes?("your country") # Handled separately # The Android embedded client could help here @@ -142,9 +142,7 @@ end def try_fetch_streaming_data(id : String, client_config : YoutubeAPI::ClientConfig) : Hash(String, JSON::Any)? LOGGER.debug("try_fetch_streaming_data: [#{id}] Using #{client_config.client_type} client.") - # CgIIAdgDAQ%3D%3D is a workaround for streaming URLs that returns a 403. - # https://github.com/LuanRT/YouTube.js/pull/624 - response = YoutubeAPI.player(video_id: id, params: "CgIIAdgDAQ%3D%3D", client_config: client_config) + response = YoutubeAPI.player(video_id: id, params: "2AMB", client_config: client_config) playability_status = response["playabilityStatus"]["status"] LOGGER.debug("try_fetch_streaming_data: [#{id}] Got playabilityStatus == #{playability_status}.") @@ -152,7 +150,7 @@ def try_fetch_streaming_data(id : String, client_config : YoutubeAPI::ClientConf if id != response.dig("videoDetails", "videoId") # YouTube may return a different video player response than expected. # See: https://github.com/TeamNewPipe/NewPipe/issues/8713 - raise VideoNotAvailableException.new( + raise InfoException.new( "The video returned by YouTube isn't the requested one. (#{client_config.client_type} client)" ) elsif playability_status == "OK" diff --git a/src/invidious/yt_backend/youtube_api.cr b/src/invidious/yt_backend/youtube_api.cr index 9e0631f6..05ccffac 100644 --- a/src/invidious/yt_backend/youtube_api.cr +++ b/src/invidious/yt_backend/youtube_api.cr @@ -8,16 +8,16 @@ module YoutubeAPI private DEFAULT_API_KEY = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8" # For Android versions, see https://en.wikipedia.org/wiki/Android_version_history - private ANDROID_APP_VERSION = "19.09.36" - private ANDROID_USER_AGENT = "com.google.android.youtube/19.09.36 (Linux; U; Android 12; US) gzip" + private ANDROID_APP_VERSION = "19.14.42" + private ANDROID_USER_AGENT = "com.google.android.youtube/19.14.42 (Linux; U; Android 12; US) gzip" private ANDROID_SDK_VERSION = 31_i64 private ANDROID_VERSION = "12" # For Apple device names, see https://gist.github.com/adamawolf/3048717 # For iOS versions, see https://en.wikipedia.org/wiki/IOS_version_history#Releases, # then go to the dedicated article of the major version you want. - private IOS_APP_VERSION = "19.09.3" - private IOS_USER_AGENT = "com.google.ios.youtube/19.09.3 (iPhone14,5; U; CPU iOS 17_4 like Mac OS X;)" + private IOS_APP_VERSION = "19.16.3" + private IOS_USER_AGENT = "com.google.ios.youtube/19.16.3 (iPhone14,5; U; CPU iOS 17_4 like Mac OS X;)" private IOS_VERSION = "17.4.0.21E219" # Major.Minor.Patch.Build private WINDOWS_VERSION = "10.0" @@ -32,6 +32,7 @@ module YoutubeAPI Android AndroidEmbeddedPlayer AndroidScreenEmbed + AndroidTestSuite IOS IOSEmbedded @@ -114,6 +115,12 @@ module YoutubeAPI os_version: ANDROID_VERSION, platform: "MOBILE", }, + ClientType::AndroidTestSuite => { + name: "ANDROID_TESTSUITE", + name_proto: "30", + version: "1.9", + api_key: DEFAULT_API_KEY, + }, # IOS From d49c76260992f210c09ca2294bdd5e8f55732247 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Fri, 26 Apr 2024 22:26:45 +0200 Subject: [PATCH 593/598] YtAPI: Add more client infos for Android test suite --- src/invidious/yt_backend/youtube_api.cr | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/invidious/yt_backend/youtube_api.cr b/src/invidious/yt_backend/youtube_api.cr index 05ccffac..bc4b90ac 100644 --- a/src/invidious/yt_backend/youtube_api.cr +++ b/src/invidious/yt_backend/youtube_api.cr @@ -6,6 +6,7 @@ module YoutubeAPI extend self private DEFAULT_API_KEY = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8" + private ANDROID_API_KEY = "AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w" # For Android versions, see https://en.wikipedia.org/wiki/Android_version_history private ANDROID_APP_VERSION = "19.14.42" @@ -13,6 +14,9 @@ module YoutubeAPI private ANDROID_SDK_VERSION = 31_i64 private ANDROID_VERSION = "12" + private ANDROID_TS_APP_VERSION = "1.9" + private ANDROID_TS_USER_AGENT = "com.google.android.youtube/1.9 (Linux; U; Android 12; US) gzip" + # For Apple device names, see https://gist.github.com/adamawolf/3048717 # For iOS versions, see https://en.wikipedia.org/wiki/IOS_version_history#Releases, # then go to the dedicated article of the major version you want. @@ -90,7 +94,7 @@ module YoutubeAPI name: "ANDROID", name_proto: "3", version: ANDROID_APP_VERSION, - api_key: "AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w", + api_key: ANDROID_API_KEY, android_sdk_version: ANDROID_SDK_VERSION, user_agent: ANDROID_USER_AGENT, os_name: "Android", @@ -116,10 +120,15 @@ module YoutubeAPI platform: "MOBILE", }, ClientType::AndroidTestSuite => { - name: "ANDROID_TESTSUITE", - name_proto: "30", - version: "1.9", - api_key: DEFAULT_API_KEY, + name: "ANDROID_TESTSUITE", + name_proto: "30", + version: ANDROID_TS_APP_VERSION, + api_key: ANDROID_API_KEY, + android_sdk_version: ANDROID_SDK_VERSION, + user_agent: ANDROID_TS_USER_AGENT, + os_name: "Android", + os_version: ANDROID_VERSION, + platform: "MOBILE", }, # IOS From be291e8f0f217c059ba35b414df4446bc00c3f27 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Fri, 26 Apr 2024 22:33:08 +0200 Subject: [PATCH 594/598] Videos: Copy captions over between responses --- src/invidious/videos/parser.cr | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr index 373f7227..ca7fb38d 100644 --- a/src/invidious/videos/parser.cr +++ b/src/invidious/videos/parser.cr @@ -123,8 +123,9 @@ def extract_video_info(video_id : String, proxy_region : String? = nil) # Replace player response and reset reason if !new_player_response.nil? - # Preserve storyboard data before replacement + # Preserve captions & storyboard data before replacement new_player_response["storyboards"] = player_response["storyboards"] if player_response["storyboards"]? + new_player_response["captions"] = player_response["captions"] if player_response["captions"]? player_response = new_player_response params.delete("reason") From 33f316c864d1bd79cd07d46db9da57a65124f31c Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Fri, 26 Apr 2024 23:15:34 +0200 Subject: [PATCH 595/598] Videos: Remove AndroidScreenEmbed client --- src/invidious/videos/parser.cr | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr index ca7fb38d..3982c3ff 100644 --- a/src/invidious/videos/parser.cr +++ b/src/invidious/videos/parser.cr @@ -109,10 +109,6 @@ def extract_video_info(video_id : String, proxy_region : String? = nil) # https://github.com/TeamNewPipe/NewPipeExtractor/issues/562 client_config.client_type = YoutubeAPI::ClientType::AndroidTestSuite new_player_response = try_fetch_streaming_data(video_id, client_config) - elsif !reason.includes?("your country") # Handled separately - # The Android embedded client could help here - client_config.client_type = YoutubeAPI::ClientType::AndroidScreenEmbed - new_player_response = try_fetch_streaming_data(video_id, client_config) end # Last hope From 79b342aee516613e3dd01db3b005922003a8cfb5 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 27 Apr 2024 00:14:46 +0200 Subject: [PATCH 596/598] Rename legacy changelog file --- CHANGELOG.md => CHANGELOG_legacy.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename CHANGELOG.md => CHANGELOG_legacy.md (100%) diff --git a/CHANGELOG.md b/CHANGELOG_legacy.md similarity index 100% rename from CHANGELOG.md rename to CHANGELOG_legacy.md From eda7444ca46dbc3941205316baba8030fe0b2989 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 27 Apr 2024 00:15:45 +0200 Subject: [PATCH 597/598] Update changelog --- CHANGELOG.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..f6f67160 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,20 @@ +# CHANGELOG + +## 2024-04-26 + +Major bug fixes: + * Videos: Use android test suite client (#4650, thanks @SamantazFox) + * Trending: Un-nest category if this is the only one (#4600, thanks @ChunkyProgrammer) + * Comments: Add support for new format (#4576, thanks @ChunkyProgrammer) + +Minor bug fixes: + * API: Add bitrate to formatStreams too (#4590, thanks @absidue) + * API: Add 'authorVerified' field on recommended videos (#4562, thanks @ChunkyProgrammer) + * Videos: Add support for new likes format (#4462, thanks @ChunkyProgrammer) + * Proxy: Handle non-200 HTTP codes on DASH manifests (#4429, thanks @absidue) + +Other improvements: + * Remove legacy proxy code (#4570, thanks @syeopite) + * API: convey info "is post live" from Youtube response (#4569, thanks @ChunkyProgrammer) + * API: Parse channel's tags (#4294, thanks @ChunkyProgrammer) + * Translations update from Hosted Weblate (#4164, thanks to our many translators) From 1ae14cc22468ce6e0eb794752b113604b1d5582d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89milien=20=28perso=29?= <4016501+unixfox@users.noreply.github.com> Date: Mon, 27 May 2024 00:40:43 +0200 Subject: [PATCH 598/598] move helm chart to a dedicated github repository (#4711) --- kubernetes/.gitignore | 1 - kubernetes/Chart.lock | 6 --- kubernetes/Chart.yaml | 22 ---------- kubernetes/README.md | 42 +------------------ kubernetes/templates/_helpers.tpl | 16 -------- kubernetes/templates/configmap.yaml | 11 ----- kubernetes/templates/deployment.yaml | 61 ---------------------------- kubernetes/templates/hpa.yaml | 18 -------- kubernetes/templates/service.yaml | 20 --------- kubernetes/values.yaml | 61 ---------------------------- 10 files changed, 1 insertion(+), 257 deletions(-) delete mode 100644 kubernetes/.gitignore delete mode 100644 kubernetes/Chart.lock delete mode 100644 kubernetes/Chart.yaml delete mode 100644 kubernetes/templates/_helpers.tpl delete mode 100644 kubernetes/templates/configmap.yaml delete mode 100644 kubernetes/templates/deployment.yaml delete mode 100644 kubernetes/templates/hpa.yaml delete mode 100644 kubernetes/templates/service.yaml delete mode 100644 kubernetes/values.yaml diff --git a/kubernetes/.gitignore b/kubernetes/.gitignore deleted file mode 100644 index 0ad51707..00000000 --- a/kubernetes/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/charts/*.tgz diff --git a/kubernetes/Chart.lock b/kubernetes/Chart.lock deleted file mode 100644 index ef12b0b6..00000000 --- a/kubernetes/Chart.lock +++ /dev/null @@ -1,6 +0,0 @@ -dependencies: -- name: postgresql - repository: https://charts.bitnami.com/bitnami/ - version: 12.11.1 -digest: sha256:3c10008175c4f5c1cec38782f5a7316154b89074c77ebbd9bcc4be4f5ff21122 -generated: "2023-09-14T22:40:43.171275362Z" diff --git a/kubernetes/Chart.yaml b/kubernetes/Chart.yaml deleted file mode 100644 index d22f6254..00000000 --- a/kubernetes/Chart.yaml +++ /dev/null @@ -1,22 +0,0 @@ -apiVersion: v2 -name: invidious -description: Invidious is an alternative front-end to YouTube -version: 1.1.1 -appVersion: 0.20.1 -keywords: -- youtube -- proxy -- video -- privacy -home: https://invidio.us/ -icon: https://raw.githubusercontent.com/iv-org/invidious/05988c1c49851b7d0094fca16aeaf6382a7f64ab/assets/favicon-32x32.png -sources: -- https://github.com/iv-org/invidious -maintainers: -- name: Leon Klingele - email: mail@leonklingele.de -dependencies: -- name: postgresql - version: ~12.11.0 - repository: "https://charts.bitnami.com/bitnami/" -engine: gotpl diff --git a/kubernetes/README.md b/kubernetes/README.md index 35478f99..e71f6a86 100644 --- a/kubernetes/README.md +++ b/kubernetes/README.md @@ -1,41 +1 @@ -# Invidious Helm chart - -Easily deploy Invidious to Kubernetes. - -## Installing Helm chart - -```sh -# Build Helm dependencies -$ helm dep build - -# Add PostgreSQL init scripts -$ kubectl create configmap invidious-postgresql-init \ - --from-file=../config/sql/channels.sql \ - --from-file=../config/sql/videos.sql \ - --from-file=../config/sql/channel_videos.sql \ - --from-file=../config/sql/users.sql \ - --from-file=../config/sql/session_ids.sql \ - --from-file=../config/sql/nonces.sql \ - --from-file=../config/sql/annotations.sql \ - --from-file=../config/sql/playlists.sql \ - --from-file=../config/sql/playlist_videos.sql - -# Install Helm app to your Kubernetes cluster -$ helm install invidious ./ -``` - -## Upgrading - -```sh -# Upgrading is easy, too! -$ helm upgrade invidious ./ -``` - -## Uninstall - -```sh -# Get rid of everything (except database) -$ helm delete invidious - -# To also delete the database, remove all invidious-postgresql PVCs -``` +The Helm chart has moved to a dedicated GitHub repository: https://github.com/iv-org/invidious-helm-chart/tree/master/invidious \ No newline at end of file diff --git a/kubernetes/templates/_helpers.tpl b/kubernetes/templates/_helpers.tpl deleted file mode 100644 index 52158b78..00000000 --- a/kubernetes/templates/_helpers.tpl +++ /dev/null @@ -1,16 +0,0 @@ -{{/* vim: set filetype=mustache: */}} -{{/* -Expand the name of the chart. -*/}} -{{- define "invidious.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} -{{- end -}} - -{{/* -Create a default fully qualified app name. -We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). -*/}} -{{- define "invidious.fullname" -}} -{{- $name := default .Chart.Name .Values.nameOverride -}} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} -{{- end -}} diff --git a/kubernetes/templates/configmap.yaml b/kubernetes/templates/configmap.yaml deleted file mode 100644 index 58542a31..00000000 --- a/kubernetes/templates/configmap.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ template "invidious.fullname" . }} - labels: - app: {{ template "invidious.name" . }} - chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" - release: {{ .Release.Name }} -data: - INVIDIOUS_CONFIG: | -{{ toYaml .Values.config | indent 4 }} diff --git a/kubernetes/templates/deployment.yaml b/kubernetes/templates/deployment.yaml deleted file mode 100644 index bb0b832f..00000000 --- a/kubernetes/templates/deployment.yaml +++ /dev/null @@ -1,61 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ template "invidious.fullname" . }} - labels: - app: {{ template "invidious.name" . }} - chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" - release: {{ .Release.Name }} -spec: - replicas: {{ .Values.replicaCount }} - selector: - matchLabels: - app: {{ template "invidious.name" . }} - release: {{ .Release.Name }} - template: - metadata: - labels: - app: {{ template "invidious.name" . }} - chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" - release: {{ .Release.Name }} - spec: - securityContext: - runAsUser: {{ .Values.securityContext.runAsUser }} - runAsGroup: {{ .Values.securityContext.runAsGroup }} - fsGroup: {{ .Values.securityContext.fsGroup }} - initContainers: - - name: wait-for-postgresql - image: postgres - args: - - /bin/sh - - -c - - until pg_isready -h {{ .Values.config.db.host }} -p {{ .Values.config.db.port }} -U {{ .Values.config.db.user }}; do echo waiting for database; sleep 2; done; - containers: - - name: {{ .Chart.Name }} - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" - imagePullPolicy: {{ .Values.image.pullPolicy }} - ports: - - containerPort: 3000 - env: - - name: INVIDIOUS_CONFIG - valueFrom: - configMapKeyRef: - key: INVIDIOUS_CONFIG - name: {{ template "invidious.fullname" . }} - securityContext: - allowPrivilegeEscalation: {{ .Values.securityContext.allowPrivilegeEscalation }} - capabilities: - drop: - - ALL - resources: -{{ toYaml .Values.resources | indent 10 }} - readinessProbe: - httpGet: - port: 3000 - path: / - livenessProbe: - httpGet: - port: 3000 - path: / - initialDelaySeconds: 15 - restartPolicy: Always diff --git a/kubernetes/templates/hpa.yaml b/kubernetes/templates/hpa.yaml deleted file mode 100644 index c6fbefe2..00000000 --- a/kubernetes/templates/hpa.yaml +++ /dev/null @@ -1,18 +0,0 @@ -{{- if .Values.autoscaling.enabled }} -apiVersion: autoscaling/v1 -kind: HorizontalPodAutoscaler -metadata: - name: {{ template "invidious.fullname" . }} - labels: - app: {{ template "invidious.name" . }} - chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" - release: {{ .Release.Name }} -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: {{ template "invidious.fullname" . }} - minReplicas: {{ .Values.autoscaling.minReplicas }} - maxReplicas: {{ .Values.autoscaling.maxReplicas }} - targetCPUUtilizationPercentage: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} -{{- end }} diff --git a/kubernetes/templates/service.yaml b/kubernetes/templates/service.yaml deleted file mode 100644 index 01454d4e..00000000 --- a/kubernetes/templates/service.yaml +++ /dev/null @@ -1,20 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ template "invidious.fullname" . }} - labels: - app: {{ template "invidious.name" . }} - chart: {{ .Chart.Name }} - release: {{ .Release.Name }} -spec: - type: {{ .Values.service.type }} - ports: - - name: http - port: {{ .Values.service.port }} - targetPort: 3000 - selector: - app: {{ template "invidious.name" . }} - release: {{ .Release.Name }} -{{- if .Values.service.loadBalancerIP }} - loadBalancerIP: {{ .Values.service.loadBalancerIP }} -{{- end }} diff --git a/kubernetes/values.yaml b/kubernetes/values.yaml deleted file mode 100644 index 5000c2b6..00000000 --- a/kubernetes/values.yaml +++ /dev/null @@ -1,61 +0,0 @@ -name: invidious - -image: - repository: quay.io/invidious/invidious - tag: latest - pullPolicy: Always - -replicaCount: 1 - -autoscaling: - enabled: false - minReplicas: 1 - maxReplicas: 16 - targetCPUUtilizationPercentage: 50 - -service: - type: ClusterIP - port: 3000 - #loadBalancerIP: - -resources: {} - #requests: - # cpu: 100m - # memory: 64Mi - #limits: - # cpu: 800m - # memory: 512Mi - -securityContext: - allowPrivilegeEscalation: false - runAsUser: 1000 - runAsGroup: 1000 - fsGroup: 1000 - -# See https://github.com/bitnami/charts/tree/master/bitnami/postgresql -postgresql: - image: - tag: 13 - auth: - username: kemal - password: kemal - database: invidious - primary: - initdb: - username: kemal - password: kemal - scriptsConfigMap: invidious-postgresql-init - -# Adapted from ../config/config.yml -config: - channel_threads: 1 - feed_threads: 1 - db: - user: kemal - password: kemal - host: invidious-postgresql - port: 5432 - dbname: invidious - full_refresh: false - https_only: false - domain: