From b0df3774dbb9494007bd5a7de62ae8cf756f3c7c Mon Sep 17 00:00:00 2001 From: src-tinkerer Date: Wed, 1 Nov 2023 21:56:25 +0330 Subject: [PATCH 001/155] Add sort options to streams --- src/invidious/channels/videos.cr | 89 ++++++++++++++++++++++--- src/invidious/routes/api/v1/channels.cr | 3 +- src/invidious/routes/channels.cr | 7 +- 3 files changed, 86 insertions(+), 13 deletions(-) diff --git a/src/invidious/channels/videos.cr b/src/invidious/channels/videos.cr index beb86e08..87af0caf 100644 --- a/src/invidious/channels/videos.cr +++ b/src/invidious/channels/videos.cr @@ -68,6 +68,76 @@ def produce_channel_videos_url(ucid, page = 1, auto_generated = nil, sort_by = " return "/browse_ajax?continuation=#{continuation}&gl=US&hl=en" end +def produce_channel_livestreams_continuation(ucid, page = 1, auto_generated = nil, sort_by = "newest", v2 = false) + object_inner_2 = { + "2:0:embedded" => { + "1:0:varint" => 0_i64, + }, + "5:varint" => 50_i64, + "6:varint" => 1_i64, + "7:varint" => (page * 30).to_i64, + "9:varint" => 1_i64, + "10:varint" => 0_i64, + } + + object_inner_2_encoded = object_inner_2 + .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) } + + sort_by_numerical = + case sort_by + when "newest" then 1_i64 + when "popular" then 2_i64 + when "oldest" then 4_i64 + else 1_i64 # Fallback to "newest" + end + + object_inner_1 = { + "110:embedded" => { + "3:embedded" => { + "14:embedded" => { + "1:embedded" => { + "1:string" => object_inner_2_encoded, + }, + "2:embedded" => { + "1:string" => "00000000-0000-0000-0000-000000000000", + }, + "3:varint" => sort_by_numerical, + }, + }, + }, + } + + object_inner_1_encoded = object_inner_1 + .try { |i| Protodec::Any.cast_json(i) } + .try { |i| Protodec::Any.from_json(i) } + .try { |i| Base64.urlsafe_encode(i) } + .try { |i| URI.encode_www_form(i) } + + object = { + "80226972:embedded" => { + "2:string" => ucid, + "3:string" => object_inner_1_encoded, + "35:string" => "browse-feed#{ucid}videos102", + }, + } + + 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 + +# Used in bypass_captcha_job.cr +def produce_channel_livestream_url(ucid, page = 1, auto_generated = nil, sort_by = "newest", v2 = false) + continuation = produce_channel_livestreams_continuation(ucid, page, auto_generated, sort_by, v2) + return "/browse_ajax?continuation=#{continuation}&gl=US&hl=en" +end + module Invidious::Channel::Tabs extend self @@ -144,21 +214,24 @@ module Invidious::Channel::Tabs # Livestreams # ------------------- - def get_livestreams(channel : AboutChannel, continuation : String? = nil) + def make_initial_livestream_ctoken(ucid, sort_by) : String + return produce_channel_livestreams_continuation(ucid, sort_by: sort_by) + end + + def get_livestreams(channel : AboutChannel, continuation : String? = nil, sort_by = "newest") if continuation.nil? - # EgdzdHJlYW1z8gYECgJ6AA%3D%3D is the protobuf object to load "streams" - initial_data = YoutubeAPI.browse(channel.ucid, params: "EgdzdHJlYW1z8gYECgJ6AA%3D%3D") - else - initial_data = YoutubeAPI.browse(continuation: continuation) + continuation ||= make_initial_livestream_ctoken(channel.ucid, sort_by) end + initial_data = YoutubeAPI.browse(continuation: continuation) + return extract_items(initial_data, channel.author, channel.ucid) end - def get_60_livestreams(channel : AboutChannel, continuation : String? = nil) + def get_60_livestreams(channel : AboutChannel, *, continuation : String? = nil, sort_by = "newest") if continuation.nil? - # Fetch the first "page" of streams - items, next_continuation = get_livestreams(channel) + # Fetch the first "page" of stream + items, next_continuation = get_livestreams(channel, sort_by: sort_by) else # Fetch a "page" of streams using the given continuation token items, next_continuation = get_livestreams(channel, continuation: continuation) diff --git a/src/invidious/routes/api/v1/channels.cr b/src/invidious/routes/api/v1/channels.cr index 67018660..f759f6ab 100644 --- a/src/invidious/routes/api/v1/channels.cr +++ b/src/invidious/routes/api/v1/channels.cr @@ -207,11 +207,12 @@ module Invidious::Routes::API::V1::Channels get_channel() # Retrieve continuation from URL parameters + sort_by = env.params.query["sort_by"]?.try &.downcase || "newest" continuation = env.params.query["continuation"]? begin videos, next_continuation = Channel::Tabs.get_60_livestreams( - channel, continuation: continuation + channel, continuation: continuation, sort_by: sort_by ) rescue ex return error_json(500, ex) diff --git a/src/invidious/routes/channels.cr b/src/invidious/routes/channels.cr index d4d8b1c1..c18644ec 100644 --- a/src/invidious/routes/channels.cr +++ b/src/invidious/routes/channels.cr @@ -81,13 +81,12 @@ module Invidious::Routes::Channels return env.redirect "/channel/#{channel.ucid}" end - # TODO: support sort option for livestreams - sort_by = "" - sort_options = [] of String + sort_by = env.params.query["sort_by"]?.try &.downcase + sort_options = {"newest", "oldest", "popular"} # Fetch items and continuation token items, next_continuation = Channel::Tabs.get_60_livestreams( - channel, continuation: continuation + channel, continuation: continuation, sort_by: (sort_by || "newest") ) selected_tab = Frontend::ChannelPage::TabsAvailable::Streams From c251c667487d4f2362d9527afb3c8d69cd089d0b Mon Sep 17 00:00:00 2001 From: karelrooted Date: Wed, 1 Nov 2023 11:40:06 +0800 Subject: [PATCH 002/155] fix youtube api vtt format subtitle for fmt=vtt to work the fmt parameter in the original caption api url need to be replaced --- src/invidious/routes/api/v1/videos.cr | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/invidious/routes/api/v1/videos.cr b/src/invidious/routes/api/v1/videos.cr index 1017ac9d..3bead06a 100644 --- a/src/invidious/routes/api/v1/videos.cr +++ b/src/invidious/routes/api/v1/videos.cr @@ -136,7 +136,11 @@ module Invidious::Routes::API::V1::Videos end end else - webvtt = YT_POOL.client &.get("#{url}&fmt=vtt").body + uri = URI.parse(url) + query_params = uri.query_params + query_params["fmt"] = "vtt" + uri.query_params = query_params + webvtt = YT_POOL.client &.get(uri.request_target).body if webvtt.starts_with?(" Date: Mon, 20 Nov 2023 15:50:59 +0330 Subject: [PATCH 003/155] Remove unused function produce_channel_livestream_url --- src/invidious/channels/videos.cr | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/invidious/channels/videos.cr b/src/invidious/channels/videos.cr index 87af0caf..c389dec3 100644 --- a/src/invidious/channels/videos.cr +++ b/src/invidious/channels/videos.cr @@ -132,12 +132,6 @@ def produce_channel_livestreams_continuation(ucid, page = 1, auto_generated = ni return continuation end -# Used in bypass_captcha_job.cr -def produce_channel_livestream_url(ucid, page = 1, auto_generated = nil, sort_by = "newest", v2 = false) - continuation = produce_channel_livestreams_continuation(ucid, page, auto_generated, sort_by, v2) - return "/browse_ajax?continuation=#{continuation}&gl=US&hl=en" -end - module Invidious::Channel::Tabs extend self From 0d63ad5a7f13316652117205b1efe2ff2bda00e6 Mon Sep 17 00:00:00 2001 From: src-tinkerer Date: Wed, 22 Nov 2023 14:50:08 +0330 Subject: [PATCH 004/155] Use a single function for fetching channel contents --- src/invidious/channels/videos.cr | 93 ++++++-------------------------- 1 file changed, 16 insertions(+), 77 deletions(-) diff --git a/src/invidious/channels/videos.cr b/src/invidious/channels/videos.cr index c389dec3..368ec2b8 100644 --- a/src/invidious/channels/videos.cr +++ b/src/invidious/channels/videos.cr @@ -1,4 +1,4 @@ -def produce_channel_videos_continuation(ucid, page = 1, auto_generated = nil, sort_by = "newest", v2 = false) +def produce_channel_content_continuation(ucid, content, page = 1, auto_generated = nil, sort_by = "newest", v2 = false) object_inner_2 = { "2:0:embedded" => { "1:0:varint" => 0_i64, @@ -16,6 +16,13 @@ def produce_channel_videos_continuation(ucid, page = 1, auto_generated = nil, so .try { |i| Base64.urlsafe_encode(i) } .try { |i| URI.encode_www_form(i) } + content_numerical = + case content + when "videos" then 15 + when "livestreams" then 14 + else 15 # Fallback to "videos" + end + sort_by_numerical = case sort_by when "newest" then 1_i64 @@ -27,7 +34,7 @@ def produce_channel_videos_continuation(ucid, page = 1, auto_generated = nil, so object_inner_1 = { "110:embedded" => { "3:embedded" => { - "15:embedded" => { + "#{content_numerical}:embedded" => { "1:embedded" => { "1:string" => object_inner_2_encoded, }, @@ -62,76 +69,16 @@ def produce_channel_videos_continuation(ucid, page = 1, auto_generated = nil, so return continuation end +def make_initial_content_ctoken(ucid, content, sort_by) : String + return produce_channel_content_continuation(ucid, content, sort_by: sort_by) +end + # Used in bypass_captcha_job.cr def produce_channel_videos_url(ucid, page = 1, auto_generated = nil, sort_by = "newest", v2 = false) - continuation = produce_channel_videos_continuation(ucid, page, auto_generated, sort_by, v2) + continuation = produce_channel_content_continuation(ucid, "videos", page, auto_generated, sort_by, v2) return "/browse_ajax?continuation=#{continuation}&gl=US&hl=en" end -def produce_channel_livestreams_continuation(ucid, page = 1, auto_generated = nil, sort_by = "newest", v2 = false) - object_inner_2 = { - "2:0:embedded" => { - "1:0:varint" => 0_i64, - }, - "5:varint" => 50_i64, - "6:varint" => 1_i64, - "7:varint" => (page * 30).to_i64, - "9:varint" => 1_i64, - "10:varint" => 0_i64, - } - - object_inner_2_encoded = object_inner_2 - .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) } - - sort_by_numerical = - case sort_by - when "newest" then 1_i64 - when "popular" then 2_i64 - when "oldest" then 4_i64 - else 1_i64 # Fallback to "newest" - end - - object_inner_1 = { - "110:embedded" => { - "3:embedded" => { - "14:embedded" => { - "1:embedded" => { - "1:string" => object_inner_2_encoded, - }, - "2:embedded" => { - "1:string" => "00000000-0000-0000-0000-000000000000", - }, - "3:varint" => sort_by_numerical, - }, - }, - }, - } - - object_inner_1_encoded = object_inner_1 - .try { |i| Protodec::Any.cast_json(i) } - .try { |i| Protodec::Any.from_json(i) } - .try { |i| Base64.urlsafe_encode(i) } - .try { |i| URI.encode_www_form(i) } - - object = { - "80226972:embedded" => { - "2:string" => ucid, - "3:string" => object_inner_1_encoded, - "35:string" => "browse-feed#{ucid}videos102", - }, - } - - 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 - module Invidious::Channel::Tabs extend self @@ -139,10 +86,6 @@ module Invidious::Channel::Tabs # Regular videos # ------------------- - def make_initial_video_ctoken(ucid, sort_by) : String - return produce_channel_videos_continuation(ucid, sort_by: sort_by) - end - # Wrapper for AboutChannel, as we still need to call get_videos with # an author name and ucid directly (e.g in RSS feeds). # TODO: figure out how to get rid of that @@ -164,7 +107,7 @@ module Invidious::Channel::Tabs end def get_videos(author : String, ucid : String, *, continuation : String? = nil, sort_by = "newest") - continuation ||= make_initial_video_ctoken(ucid, sort_by) + continuation ||= make_initial_content_ctoken(ucid, "videos", sort_by) initial_data = YoutubeAPI.browse(continuation: continuation) return extract_items(initial_data, author, ucid) @@ -208,13 +151,9 @@ module Invidious::Channel::Tabs # Livestreams # ------------------- - def make_initial_livestream_ctoken(ucid, sort_by) : String - return produce_channel_livestreams_continuation(ucid, sort_by: sort_by) - end - def get_livestreams(channel : AboutChannel, continuation : String? = nil, sort_by = "newest") if continuation.nil? - continuation ||= make_initial_livestream_ctoken(channel.ucid, sort_by) + continuation ||= make_initial_content_ctoken(channel.ucid, "livestreams", sort_by) end initial_data = YoutubeAPI.browse(continuation: continuation) From 162b89d9427ec4cbeb4661d7a7866847ba582880 Mon Sep 17 00:00:00 2001 From: src-tinkerer Date: Thu, 23 Nov 2023 14:44:37 +0330 Subject: [PATCH 005/155] Fix format in videos.cr --- src/invidious/channels/videos.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/invidious/channels/videos.cr b/src/invidious/channels/videos.cr index 368ec2b8..80ec498c 100644 --- a/src/invidious/channels/videos.cr +++ b/src/invidious/channels/videos.cr @@ -18,9 +18,9 @@ def produce_channel_content_continuation(ucid, content, page = 1, auto_generated content_numerical = case content - when "videos" then 15 + when "videos" then 15 when "livestreams" then 14 - else 15 # Fallback to "videos" + else 15 # Fallback to "videos" end sort_by_numerical = From 6251d8d43f8b0491e4ff86fa8433b92a91b5fafd Mon Sep 17 00:00:00 2001 From: src-tinkerer Date: Sat, 25 Nov 2023 00:46:11 +0330 Subject: [PATCH 006/155] Rename a variable in videos.cr --- src/invidious/channels/videos.cr | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/invidious/channels/videos.cr b/src/invidious/channels/videos.cr index 80ec498c..cc683788 100644 --- a/src/invidious/channels/videos.cr +++ b/src/invidious/channels/videos.cr @@ -1,4 +1,4 @@ -def produce_channel_content_continuation(ucid, content, page = 1, auto_generated = nil, sort_by = "newest", v2 = false) +def produce_channel_content_continuation(ucid, content_type, page = 1, auto_generated = nil, sort_by = "newest", v2 = false) object_inner_2 = { "2:0:embedded" => { "1:0:varint" => 0_i64, @@ -16,8 +16,8 @@ def produce_channel_content_continuation(ucid, content, page = 1, auto_generated .try { |i| Base64.urlsafe_encode(i) } .try { |i| URI.encode_www_form(i) } - content_numerical = - case content + content_type_numerical = + case content_type when "videos" then 15 when "livestreams" then 14 else 15 # Fallback to "videos" @@ -34,7 +34,7 @@ def produce_channel_content_continuation(ucid, content, page = 1, auto_generated object_inner_1 = { "110:embedded" => { "3:embedded" => { - "#{content_numerical}:embedded" => { + "#{content_type_numerical}:embedded" => { "1:embedded" => { "1:string" => object_inner_2_encoded, }, @@ -69,8 +69,8 @@ def produce_channel_content_continuation(ucid, content, page = 1, auto_generated return continuation end -def make_initial_content_ctoken(ucid, content, sort_by) : String - return produce_channel_content_continuation(ucid, content, sort_by: sort_by) +def make_initial_content_ctoken(ucid, content_type, sort_by) : String + return produce_channel_content_continuation(ucid, content_type, sort_by: sort_by) end # Used in bypass_captcha_job.cr From 5f2b43d6534fcbb12d9c66c4e99b16863bd29893 Mon Sep 17 00:00:00 2001 From: src-tinkerer Date: Sat, 25 Nov 2023 00:48:27 +0330 Subject: [PATCH 007/155] Remove unecessary if condition in videos.cr --- src/invidious/channels/videos.cr | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/invidious/channels/videos.cr b/src/invidious/channels/videos.cr index cc683788..02fc4d2f 100644 --- a/src/invidious/channels/videos.cr +++ b/src/invidious/channels/videos.cr @@ -152,9 +152,7 @@ module Invidious::Channel::Tabs # ------------------- def get_livestreams(channel : AboutChannel, continuation : String? = nil, sort_by = "newest") - if continuation.nil? - continuation ||= make_initial_content_ctoken(channel.ucid, "livestreams", sort_by) - end + continuation ||= make_initial_content_ctoken(channel.ucid, "livestreams", sort_by) initial_data = YoutubeAPI.browse(continuation: continuation) From 1363fb809436464de57b90113864ff50867a9dae Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Tue, 28 Nov 2023 21:34:17 -0500 Subject: [PATCH 008/155] Fix error code for disabled popular endpoint --- src/invidious/routes/api/v1/feeds.cr | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/invidious/routes/api/v1/feeds.cr b/src/invidious/routes/api/v1/feeds.cr index 41865f34..0ee22ca6 100644 --- a/src/invidious/routes/api/v1/feeds.cr +++ b/src/invidious/routes/api/v1/feeds.cr @@ -30,8 +30,7 @@ module Invidious::Routes::API::V1::Feeds env.response.content_type = "application/json" if !CONFIG.popular_enabled - error_message = {"error" => "Administrator has disabled this endpoint."}.to_json - haltf env, 400, error_message + return error_json(403, "Administrator has disabled this endpoint.") end JSON.build do |json| From cf61af67abc1bffdeefd080542d11c2eae13d754 Mon Sep 17 00:00:00 2001 From: src-tinkerer Date: Thu, 30 Nov 2023 14:34:01 +0330 Subject: [PATCH 009/155] Update src/invidious/routes/channels.cr sort_by for consistency --- src/invidious/routes/channels.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/invidious/routes/channels.cr b/src/invidious/routes/channels.cr index c18644ec..b5b4ad96 100644 --- a/src/invidious/routes/channels.cr +++ b/src/invidious/routes/channels.cr @@ -81,12 +81,12 @@ module Invidious::Routes::Channels return env.redirect "/channel/#{channel.ucid}" end - sort_by = env.params.query["sort_by"]?.try &.downcase + sort_by = env.params.query["sort_by"]?.try &.downcase || "newest" sort_options = {"newest", "oldest", "popular"} # Fetch items and continuation token items, next_continuation = Channel::Tabs.get_60_livestreams( - channel, continuation: continuation, sort_by: (sort_by || "newest") + channel, continuation: continuation, sort_by: sort_by ) selected_tab = Frontend::ChannelPage::TabsAvailable::Streams 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 010/155] 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 4adb4c00d2099ad7892579bfe4777d6f64a807a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Wilczy=C5=84ski?= Date: Sat, 24 Feb 2024 20:01:16 +0100 Subject: [PATCH 011/155] routes: Allow embedding videos in local HTML files (fixes #4448) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The current Content Security Policy does not allow to embed videos inside local HTML files which are viewed in the browser via the file protocol. This commit adds the file protocol to the allowed frame ancestors, so that the embedded videos load correctly in local HTML files. This behaviour is consistent which how the official YouTube website allows to embed videos from itself. Signed-off-by: Tomasz Wilczyński --- src/invidious/routes/before_all.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/routes/before_all.cr b/src/invidious/routes/before_all.cr index 396840a4..5695dee9 100644 --- a/src/invidious/routes/before_all.cr +++ b/src/invidious/routes/before_all.cr @@ -30,7 +30,7 @@ module Invidious::Routes::BeforeAll # Only allow the pages at /embed/* to be embedded if env.request.resource.starts_with?("/embed") - frame_ancestors = "'self' http: https:" + frame_ancestors = "'self' file: http: https:" else frame_ancestors = "'none'" end 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 012/155] 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 013/155] 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 499aed37dd4f6033cb35796c4c3a08dad65659be Mon Sep 17 00:00:00 2001 From: nooptek Date: Sat, 19 Aug 2023 00:25:54 +0200 Subject: [PATCH 014/155] Fix handling of modified source code URL setting --- src/invidious/routes/preferences.cr | 2 +- src/invidious/views/user/preferences.ecr | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/invidious/routes/preferences.cr b/src/invidious/routes/preferences.cr index 112535bd..05bc2714 100644 --- a/src/invidious/routes/preferences.cr +++ b/src/invidious/routes/preferences.cr @@ -214,7 +214,7 @@ module Invidious::Routes::PreferencesRoute statistics_enabled ||= "off" CONFIG.statistics_enabled = statistics_enabled == "on" - CONFIG.modified_source_code_url = env.params.body["modified_source_code_url"]?.try &.as(String) + CONFIG.modified_source_code_url = env.params.body["modified_source_code_url"]?.presence File.write("config/config.yml", CONFIG.to_yaml) end diff --git a/src/invidious/views/user/preferences.ecr b/src/invidious/views/user/preferences.ecr index 55349c5a..b89c73ca 100644 --- a/src/invidious/views/user/preferences.ecr +++ b/src/invidious/views/user/preferences.ecr @@ -310,7 +310,7 @@
- checked<% end %>> +
<% end %> 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 015/155] 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 016/155] 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 c5eb10b21f742041ad3dad809a0f5aa6ce339c17 Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Mon, 1 Apr 2024 10:02:49 -0400 Subject: [PATCH 017/155] Revert "Fix error code for disabled popular endpoint" This reverts commit 1363fb809436464de57b90113864ff50867a9dae. --- src/invidious/routes/api/v1/feeds.cr | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/invidious/routes/api/v1/feeds.cr b/src/invidious/routes/api/v1/feeds.cr index 0ee22ca6..41865f34 100644 --- a/src/invidious/routes/api/v1/feeds.cr +++ b/src/invidious/routes/api/v1/feeds.cr @@ -30,7 +30,8 @@ module Invidious::Routes::API::V1::Feeds env.response.content_type = "application/json" if !CONFIG.popular_enabled - return error_json(403, "Administrator has disabled this endpoint.") + error_message = {"error" => "Administrator has disabled this endpoint."}.to_json + haltf env, 400, error_message end JSON.build do |json| From b0c6bdf44c7fdff97d4fd408a7fede67f82e68a6 Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Mon, 1 Apr 2024 10:03:29 -0400 Subject: [PATCH 018/155] use 403 code --- src/invidious/routes/api/v1/feeds.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/routes/api/v1/feeds.cr b/src/invidious/routes/api/v1/feeds.cr index 41865f34..fea2993c 100644 --- a/src/invidious/routes/api/v1/feeds.cr +++ b/src/invidious/routes/api/v1/feeds.cr @@ -31,7 +31,7 @@ module Invidious::Routes::API::V1::Feeds if !CONFIG.popular_enabled error_message = {"error" => "Administrator has disabled this endpoint."}.to_json - haltf env, 400, error_message + haltf env, 403, error_message end JSON.build do |json| 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 019/155] 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 020/155] 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 021/155] 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 022/155] 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 023/155] 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 024/155] 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 025/155] 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 026/155] 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 027/155] 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 028/155] 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 029/155] 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 b90cf286fc947ed265031907bdb786986a399c41 Mon Sep 17 00:00:00 2001 From: absidue <48293849+absidue@users.noreply.github.com> Date: Sat, 20 Apr 2024 20:46:01 +0200 Subject: [PATCH 030/155] Fix duplicate query parameters in URLs when local=true for /api/v1/videos/{id} --- src/invidious/http_server/utils.cr | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/invidious/http_server/utils.cr b/src/invidious/http_server/utils.cr index 222dfc4a..623a9177 100644 --- a/src/invidious/http_server/utils.cr +++ b/src/invidious/http_server/utils.cr @@ -11,11 +11,12 @@ module Invidious::HttpServer params = url.query_params params["host"] = url.host.not_nil! # Should never be nil, in theory params["region"] = region if !region.nil? + url.query_params = params if absolute - return "#{HOST_URL}#{url.request_target}?#{params}" + return "#{HOST_URL}#{url.request_target}" else - return "#{url.request_target}?#{params}" + return url.request_target end end 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 031/155] 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 032/155] 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 033/155] 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 034/155] 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 035/155] 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 036/155] 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 037/155] 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 038/155] 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 039/155] 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 040/155] 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 041/155] 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 042/155] 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 043/155] 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 044/155] 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 045/155] 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 046/155] 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 047/155] 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 048/155] 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 049/155] 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 050/155] 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 051/155] 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 052/155] 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 053/155] 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 054/155] 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 055/155] 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 056/155] 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 057/155] 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 058/155] 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 059/155] 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 060/155] 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 061/155] 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 062/155] 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 063/155] 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 064/155] 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 065/155] 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 066/155] 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 067/155] 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 068/155] 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 b0ec359028c05367d3aa5b230a9cbf87de661191 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 27 Apr 2024 20:01:19 +0200 Subject: [PATCH 069/155] CI: Bump Crystal version matrix --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 057e4d61..08d2e16b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,10 +38,10 @@ jobs: matrix: stable: [true] crystal: - - 1.7.3 - - 1.8.2 - 1.9.2 - 1.10.1 + - 1.11.2 + - 1.12.1 include: - crystal: nightly stable: false From 470245de54f9a2941cdab15a55edb074f1a9897f Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 27 Apr 2024 20:48:42 +0200 Subject: [PATCH 070/155] YtAPI: Remove API keys like official clients --- src/invidious/yt_backend/youtube_api.cr | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/src/invidious/yt_backend/youtube_api.cr b/src/invidious/yt_backend/youtube_api.cr index 727ce9a3..c8b037c8 100644 --- a/src/invidious/yt_backend/youtube_api.cr +++ b/src/invidious/yt_backend/youtube_api.cr @@ -5,9 +5,6 @@ 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" private ANDROID_USER_AGENT = "com.google.android.youtube/19.14.42 (Linux; U; Android 12; US) gzip" @@ -52,7 +49,6 @@ module YoutubeAPI name: "WEB", name_proto: "1", version: "2.20240304.00.00", - api_key: DEFAULT_API_KEY, screen: "WATCH_FULL_SCREEN", os_name: "Windows", os_version: WINDOWS_VERSION, @@ -62,7 +58,6 @@ module YoutubeAPI name: "WEB_EMBEDDED_PLAYER", name_proto: "56", version: "1.20240303.00.00", - api_key: DEFAULT_API_KEY, screen: "EMBED", os_name: "Windows", os_version: WINDOWS_VERSION, @@ -72,7 +67,6 @@ module YoutubeAPI name: "MWEB", name_proto: "2", version: "2.20240304.08.00", - api_key: DEFAULT_API_KEY, os_name: "Android", os_version: ANDROID_VERSION, platform: "MOBILE", @@ -81,7 +75,6 @@ module YoutubeAPI name: "WEB", name_proto: "1", version: "2.20240304.00.00", - api_key: DEFAULT_API_KEY, screen: "EMBED", os_name: "Windows", os_version: WINDOWS_VERSION, @@ -94,7 +87,6 @@ module YoutubeAPI name: "ANDROID", name_proto: "3", version: ANDROID_APP_VERSION, - api_key: ANDROID_API_KEY, android_sdk_version: ANDROID_SDK_VERSION, user_agent: ANDROID_USER_AGENT, os_name: "Android", @@ -105,13 +97,11 @@ module YoutubeAPI name: "ANDROID_EMBEDDED_PLAYER", name_proto: "55", version: ANDROID_APP_VERSION, - api_key: "AIzaSyCjc_pVEDi4qsv5MtC2dMXzpIaDoRFLsxw", }, ClientType::AndroidScreenEmbed => { name: "ANDROID", name_proto: "3", version: ANDROID_APP_VERSION, - api_key: DEFAULT_API_KEY, screen: "EMBED", android_sdk_version: ANDROID_SDK_VERSION, user_agent: ANDROID_USER_AGENT, @@ -123,7 +113,6 @@ module YoutubeAPI 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", @@ -137,7 +126,6 @@ module YoutubeAPI name: "IOS", name_proto: "5", version: IOS_APP_VERSION, - api_key: "AIzaSyB-63vPrdThhKuerbB2N_l7Kwwcxj6yUAc", user_agent: IOS_USER_AGENT, device_make: "Apple", device_model: "iPhone14,5", @@ -149,7 +137,6 @@ module YoutubeAPI name: "IOS_MESSAGES_EXTENSION", name_proto: "66", version: IOS_APP_VERSION, - api_key: DEFAULT_API_KEY, user_agent: IOS_USER_AGENT, device_make: "Apple", device_model: "iPhone14,5", @@ -161,7 +148,6 @@ module YoutubeAPI name: "IOS_MUSIC", name_proto: "26", version: "6.42", - api_key: "AIzaSyBAETezhkwP0ZWA02RsqT1zu78Fpt0bC_s", 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", @@ -176,13 +162,11 @@ module YoutubeAPI name: "TVHTML5", name_proto: "7", version: "7.20240304.10.00", - api_key: DEFAULT_API_KEY, }, ClientType::TvHtml5ScreenEmbed => { name: "TVHTML5_SIMPLY_EMBEDDED_PLAYER", name_proto: "85", version: "2.0", - api_key: DEFAULT_API_KEY, screen: "EMBED", }, } @@ -237,11 +221,6 @@ module YoutubeAPI HARDCODED_CLIENTS[@client_type][:version] end - # :ditto: - def api_key : String - HARDCODED_CLIENTS[@client_type][:api_key] - end - # :ditto: def screen : String HARDCODED_CLIENTS[@client_type][:screen]? || "" @@ -606,7 +585,7 @@ module YoutubeAPI client_config ||= DEFAULT_CLIENT_CONFIG # Query parameters - url = "#{endpoint}?key=#{client_config.api_key}&prettyPrint=false" + url = "#{endpoint}?prettyPrint=false" headers = HTTP::Headers{ "Content-Type" => "application/json; charset=UTF-8", From 2fdb6dd6441287e51abe06f4d9e6ce0586f916ff Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 27 Apr 2024 21:02:37 +0200 Subject: [PATCH 071/155] CI: Bump Crystal version in docker too --- .github/workflows/container-release.yml | 2 +- docker/Dockerfile | 2 +- docker/Dockerfile.arm64 | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/container-release.yml b/.github/workflows/container-release.yml index e44ac200..795995e9 100644 --- a/.github/workflows/container-release.yml +++ b/.github/workflows/container-release.yml @@ -26,7 +26,7 @@ jobs: - name: Install Crystal uses: crystal-lang/install-crystal@v1.8.0 with: - crystal: 1.9.2 + crystal: 1.12.1 - name: Run lint run: | diff --git a/docker/Dockerfile b/docker/Dockerfile index ace096bf..3d9323fd 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM crystallang/crystal:1.8.2-alpine AS builder +FROM crystallang/crystal:1.12.1-alpine AS builder RUN apk add --no-cache sqlite-static yaml-static diff --git a/docker/Dockerfile.arm64 b/docker/Dockerfile.arm64 index 602f3ab2..f054b326 100644 --- a/docker/Dockerfile.arm64 +++ b/docker/Dockerfile.arm64 @@ -1,5 +1,5 @@ -FROM alpine:3.18 AS builder -RUN apk add --no-cache 'crystal=1.8.2-r0' shards sqlite-static yaml-static yaml-dev libxml2-static zlib-static openssl-libs-static openssl-dev musl-dev xz-static +FROM alpine:3.19 AS builder +RUN apk add --no-cache 'crystal=1.10.1-r0' shards sqlite-static yaml-static yaml-dev libxml2-static zlib-static openssl-libs-static openssl-dev musl-dev xz-static ARG release From f696f9682486b4af0aaf18eca6ced1a2a4c08dde Mon Sep 17 00:00:00 2001 From: ulmemxpoc <123284914+ulmemxpoc@users.noreply.github.com> Date: Tue, 30 Apr 2024 03:40:19 +0000 Subject: [PATCH 072/155] Add rel="noreferrer noopener" to external links --- src/invidious/frontend/comments_youtube.cr | 4 ++-- src/invidious/helpers/errors.cr | 2 +- src/invidious/views/components/video-context-buttons.ecr | 2 +- src/invidious/views/playlist.ecr | 2 +- src/invidious/views/watch.ecr | 6 +++--- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/invidious/frontend/comments_youtube.cr b/src/invidious/frontend/comments_youtube.cr index aecac87f..f9eb44ef 100644 --- a/src/invidious/frontend/comments_youtube.cr +++ b/src/invidious/frontend/comments_youtube.cr @@ -149,12 +149,12 @@ module Invidious::Frontend::Comments if comments["videoId"]? html << <<-END_HTML - [YT] + [YT] | END_HTML elsif comments["authorId"]? html << <<-END_HTML - [YT] + [YT] | END_HTML end diff --git a/src/invidious/helpers/errors.cr b/src/invidious/helpers/errors.cr index 21b789bc..b2df682d 100644 --- a/src/invidious/helpers/errors.cr +++ b/src/invidious/helpers/errors.cr @@ -190,7 +190,7 @@ def error_redirect_helper(env : HTTP::Server::Context) #{switch_instance}
  • - #{go_to_youtube} + #{go_to_youtube}
  • END_HTML diff --git a/src/invidious/views/components/video-context-buttons.ecr b/src/invidious/views/components/video-context-buttons.ecr index 385ed6b3..22458a03 100644 --- a/src/invidious/views/components/video-context-buttons.ecr +++ b/src/invidious/views/components/video-context-buttons.ecr @@ -1,6 +1,6 @@
    - " href="https://www.youtube.com/watch<%=endpoint_params%>"> + " rel="noreferrer noopener" href="https://www.youtube.com/watch<%=endpoint_params%>"> " href="/watch<%=endpoint_params%>&listen=1"> diff --git a/src/invidious/views/playlist.ecr b/src/invidious/views/playlist.ecr index 24ba437d..c27ddba6 100644 --- a/src/invidious/views/playlist.ecr +++ b/src/invidious/views/playlist.ecr @@ -83,7 +83,7 @@ <% if !playlist.is_a? InvidiousPlaylist %>
    - + <%= translate(locale, "View playlist on YouTube") %> | diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 7a1cf2c3..586b4cff 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -26,7 +26,7 @@ - + <%= rendered "components/player_sources" %> <%= title %> - Invidious @@ -123,8 +123,8 @@ we're going to need to do it here in order to allow for translations. 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") %>) + <%= translate(locale, "videoinfo_watch_on_youTube") %> + (<%= translate(locale, "videoinfo_youTube_embed_link") %>)

    From 9d66676f2dbb18a87ca7515e839f1c64688ecd39 Mon Sep 17 00:00:00 2001 From: Fijxu Date: Wed, 1 May 2024 22:17:41 -0400 Subject: [PATCH 073/155] Use full URL in the og:image property. --- src/invidious/views/channel.ecr | 4 ++-- src/invidious/views/watch.ecr | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/invidious/views/channel.ecr b/src/invidious/views/channel.ecr index 09df106d..a84e44bc 100644 --- a/src/invidious/views/channel.ecr +++ b/src/invidious/views/channel.ecr @@ -30,13 +30,13 @@ - + - + <%- end -%> diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 7a1cf2c3..9e7467dd 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -10,7 +10,7 @@ - + From c4fec89a9bac0228f6fac6ab2e8547132b57cc98 Mon Sep 17 00:00:00 2001 From: ulmemxpoc <123284914+ulmemxpoc@users.noreply.github.com> Date: Fri, 10 May 2024 11:23:11 -0700 Subject: [PATCH 074/155] Apply suggestions from code review --- src/invidious/frontend/comments_youtube.cr | 2 +- src/invidious/views/watch.ecr | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/invidious/frontend/comments_youtube.cr b/src/invidious/frontend/comments_youtube.cr index f9eb44ef..a0e1d783 100644 --- a/src/invidious/frontend/comments_youtube.cr +++ b/src/invidious/frontend/comments_youtube.cr @@ -149,7 +149,7 @@ module Invidious::Frontend::Comments if comments["videoId"]? html << <<-END_HTML - [YT] + [YT] | END_HTML elsif comments["authorId"]? diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 586b4cff..fd9e1592 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -26,7 +26,7 @@ - + <%= rendered "components/player_sources" %> <%= title %> - Invidious From 90fcf80a8d20b07e18070800474e0fc8ee342020 Mon Sep 17 00:00:00 2001 From: Fijxu Date: Mon, 13 May 2024 19:27:27 -0400 Subject: [PATCH 075/155] Handle playlists cataloged as Podcast Videos of a playlist cataloged as podcast are called episodes therefore Invidious was not able to find `video` in the `text` value inside the stats array. --- src/invidious/playlists.cr | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/invidious/playlists.cr b/src/invidious/playlists.cr index 955e0855..a227f794 100644 --- a/src/invidious/playlists.cr +++ b/src/invidious/playlists.cr @@ -366,6 +366,8 @@ def fetch_playlist(plid : String) if text.includes? "video" video_count = text.gsub(/\D/, "").to_i? || 0 + elsif text.includes? "episode" + video_count = text.gsub(/\D/, "").to_i? || 0 elsif text.includes? "view" views = text.gsub(/\D/, "").to_i64? || 0_i64 else From e0d0dbde3cd1cba313d990244977a890a32976de Mon Sep 17 00:00:00 2001 From: Fijxu Date: Mon, 13 May 2024 21:07:46 -0400 Subject: [PATCH 076/155] API: Check if playlist has any videos on it. Invidious assumes that every playlist will have at least one video because it needs to check for the `index` key. So if there is no videos on a playlist, there is no `index` key and Invidious throws `Index out of bounds` --- src/invidious/routes/api/v1/misc.cr | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/invidious/routes/api/v1/misc.cr b/src/invidious/routes/api/v1/misc.cr index 12942906..0c79692d 100644 --- a/src/invidious/routes/api/v1/misc.cr +++ b/src/invidious/routes/api/v1/misc.cr @@ -74,7 +74,9 @@ module Invidious::Routes::API::V1::Misc response = playlist.to_json(offset, video_id: video_id) json_response = JSON.parse(response) - if json_response["videos"].as_a[0]["index"] != offset + if json_response["videos"].as_a.empty? + json_response = JSON.parse(response) + elsif json_response["videos"].as_a[0]["index"] != offset offset = json_response["videos"].as_a[0]["index"].as_i lookback = offset < 50 ? offset : 50 response = playlist.to_json(offset - lookback) From 71a821a7e65de56ba4816bb07380cebf9914c00a Mon Sep 17 00:00:00 2001 From: absidue <48293849+absidue@users.noreply.github.com> Date: Sat, 20 Apr 2024 18:50:17 +0200 Subject: [PATCH 077/155] Return actual height, width and fps for streams in /api/v1/videos --- src/invidious/jsonify/api_v1/video_json.cr | 75 ++++++++++++---------- 1 file changed, 42 insertions(+), 33 deletions(-) diff --git a/src/invidious/jsonify/api_v1/video_json.cr b/src/invidious/jsonify/api_v1/video_json.cr index 0dced80b..8c1f5c3c 100644 --- a/src/invidious/jsonify/api_v1/video_json.cr +++ b/src/invidious/jsonify/api_v1/video_json.cr @@ -114,25 +114,30 @@ module Invidious::JSONify::APIv1 json.field "projectionType", fmt["projectionType"] - if fmt_info = Invidious::Videos::Formats.itag_to_metadata?(fmt["itag"]) - fps = fmt_info["fps"]?.try &.to_i || fmt["fps"]?.try &.as_i || 30 + height = fmt["height"]?.try &.as_i + width = fmt["width"]?.try &.as_i + + fps = fmt["fps"]?.try &.as_i + + if fps json.field "fps", fps + end + + if height && width + json.field "size", "#{width}x#{height}" + + quality_label = "#{width > height ? height : width}" + + if fps && fps > 30 + quality_label += fps.to_s + end + + json.field "qualityLabel", quality_label + end + + if fmt_info = Invidious::Videos::Formats.itag_to_metadata?(fmt["itag"]) json.field "container", fmt_info["ext"] json.field "encoding", fmt_info["vcodec"]? || fmt_info["acodec"] - - if fmt_info["height"]? - json.field "resolution", "#{fmt_info["height"]}p" - - quality_label = "#{fmt_info["height"]}p" - if fps > 30 - quality_label += "60" - end - json.field "qualityLabel", quality_label - - if fmt_info["width"]? - json.field "size", "#{fmt_info["width"]}x#{fmt_info["height"]}" - end - end end # Livestream chunk infos @@ -163,26 +168,30 @@ module Invidious::JSONify::APIv1 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 + height = fmt["height"]?.try &.as_i + width = fmt["width"]?.try &.as_i + + fps = fmt["fps"]?.try &.as_i + + if fps json.field "fps", fps + end + + if height && width + json.field "size", "#{width}x#{height}" + + quality_label = "#{width > height ? height : width}" + + if fps && fps > 30 + quality_label += fps.to_s + end + + json.field "qualityLabel", quality_label + end + + if fmt_info = Invidious::Videos::Formats.itag_to_metadata?(fmt["itag"]) json.field "container", fmt_info["ext"] json.field "encoding", fmt_info["vcodec"]? || fmt_info["acodec"] - - if fmt_info["height"]? - json.field "resolution", "#{fmt_info["height"]}p" - - quality_label = "#{fmt_info["height"]}p" - if fps > 30 - quality_label += "60" - end - json.field "qualityLabel", quality_label - - if fmt_info["width"]? - json.field "size", "#{fmt_info["width"]}x#{fmt_info["height"]}" - end - end end end end From f57aac5815e20bed2b495cb1994f4d8d50654b61 Mon Sep 17 00:00:00 2001 From: absidue <48293849+absidue@users.noreply.github.com> Date: Sun, 21 Apr 2024 14:58:12 +0200 Subject: [PATCH 078/155] Fix the missing `p` in the quality labels. Co-authored-by: Samantaz Fox --- src/invidious/jsonify/api_v1/video_json.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/invidious/jsonify/api_v1/video_json.cr b/src/invidious/jsonify/api_v1/video_json.cr index 8c1f5c3c..7f17f35a 100644 --- a/src/invidious/jsonify/api_v1/video_json.cr +++ b/src/invidious/jsonify/api_v1/video_json.cr @@ -126,7 +126,7 @@ module Invidious::JSONify::APIv1 if height && width json.field "size", "#{width}x#{height}" - quality_label = "#{width > height ? height : width}" + quality_label = "#{width > height ? height : width}p" if fps && fps > 30 quality_label += fps.to_s @@ -180,7 +180,7 @@ module Invidious::JSONify::APIv1 if height && width json.field "size", "#{width}x#{height}" - quality_label = "#{width > height ? height : width}" + quality_label = "#{width > height ? height : width}p" if fps && fps > 30 quality_label += fps.to_s From 57e606cb43d43c627708f0538eddcde3b0f580a0 Mon Sep 17 00:00:00 2001 From: absidue <48293849+absidue@users.noreply.github.com> Date: Thu, 25 Apr 2024 21:38:51 +0200 Subject: [PATCH 079/155] Add back missing resolution field --- 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 7f17f35a..6e8c3a72 100644 --- a/src/invidious/jsonify/api_v1/video_json.cr +++ b/src/invidious/jsonify/api_v1/video_json.cr @@ -125,6 +125,7 @@ module Invidious::JSONify::APIv1 if height && width json.field "size", "#{width}x#{height}" + json.field "resolution" "#{height}p" quality_label = "#{width > height ? height : width}p" @@ -179,6 +180,7 @@ module Invidious::JSONify::APIv1 if height && width json.field "size", "#{width}x#{height}" + json.field "resolution" "#{height}p" quality_label = "#{width > height ? height : width}p" From 3b773c4f77c1469bcd158f7ab912fcb57af7b014 Mon Sep 17 00:00:00 2001 From: absidue <48293849+absidue@users.noreply.github.com> Date: Thu, 25 Apr 2024 21:51:19 +0200 Subject: [PATCH 080/155] Fix missing commas --- src/invidious/jsonify/api_v1/video_json.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/invidious/jsonify/api_v1/video_json.cr b/src/invidious/jsonify/api_v1/video_json.cr index 6e8c3a72..59714828 100644 --- a/src/invidious/jsonify/api_v1/video_json.cr +++ b/src/invidious/jsonify/api_v1/video_json.cr @@ -125,7 +125,7 @@ module Invidious::JSONify::APIv1 if height && width json.field "size", "#{width}x#{height}" - json.field "resolution" "#{height}p" + json.field "resolution", "#{height}p" quality_label = "#{width > height ? height : width}p" @@ -180,7 +180,7 @@ module Invidious::JSONify::APIv1 if height && width json.field "size", "#{width}x#{height}" - json.field "resolution" "#{height}p" + json.field "resolution", "#{height}p" quality_label = "#{width > height ? height : width}p" 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 081/155] 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: From 31ad708206dc108714e36f617c6bce5f85c80b8b Mon Sep 17 00:00:00 2001 From: meatball Date: Thu, 30 May 2024 21:56:33 +0200 Subject: [PATCH 082/155] fix: Handle nil value for genreUcid in Video struct --- src/invidious/videos.cr | 2 +- src/invidious/videos/parser.cr | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index c218b4ef..cdfca02c 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -250,7 +250,7 @@ struct Video end def genre_url : String? - info["genreUcid"]? ? "/channel/#{info["genreUcid"]}" : nil + info["genreUcid"].try &.as_s? ? "/channel/#{info["genreUcid"]}" : nil end def is_vr : Bool? diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr index 0e1a947c..bc3c844d 100644 --- a/src/invidious/videos/parser.cr +++ b/src/invidious/videos/parser.cr @@ -327,7 +327,7 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any if metadata_title == "Category" contents = contents.try &.dig?("runs", 0) - + genre = contents.try &.["text"]? genre_ucid = contents.try &.dig?("navigationEndpoint", "browseEndpoint", "browseId") elsif metadata_title == "License" @@ -424,7 +424,7 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any "shortDescription" => JSON::Any.new(short_description.try &.as_s || nil), # Video metadata "genre" => JSON::Any.new(genre.try &.as_s || ""), - "genreUcid" => JSON::Any.new(genre_ucid.try &.as_s || ""), + "genreUcid" => JSON::Any.new(genre_ucid.try &.as_s || nil), "license" => JSON::Any.new(license.try &.as_s || ""), # Music section "music" => JSON.parse(music_list.to_json), From 629599f9403a4b5b5ceda58f2d17ad81745f6981 Mon Sep 17 00:00:00 2001 From: meatball Date: Thu, 30 May 2024 21:57:15 +0200 Subject: [PATCH 083/155] Fix change in parser file --- src/invidious/videos/parser.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr index bc3c844d..85f17525 100644 --- a/src/invidious/videos/parser.cr +++ b/src/invidious/videos/parser.cr @@ -327,7 +327,7 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any if metadata_title == "Category" contents = contents.try &.dig?("runs", 0) - + genre = contents.try &.["text"]? genre_ucid = contents.try &.dig?("navigationEndpoint", "browseEndpoint", "browseId") elsif metadata_title == "License" From 59575236243cb28f3e0199e028a9042970f133ba Mon Sep 17 00:00:00 2001 From: meatball Date: Thu, 30 May 2024 22:13:30 +0200 Subject: [PATCH 084/155] Improve code quallity --- src/invidious/videos/parser.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr index 85f17525..4bdb2512 100644 --- a/src/invidious/videos/parser.cr +++ b/src/invidious/videos/parser.cr @@ -424,7 +424,7 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any "shortDescription" => JSON::Any.new(short_description.try &.as_s || nil), # Video metadata "genre" => JSON::Any.new(genre.try &.as_s || ""), - "genreUcid" => JSON::Any.new(genre_ucid.try &.as_s || nil), + "genreUcid" => JSON::Any.new(genre_ucid.try &.as_s?), "license" => JSON::Any.new(license.try &.as_s || ""), # Music section "music" => JSON.parse(music_list.to_json), From 04ca64691b76b432374e4bb3dcde64cc37a97869 Mon Sep 17 00:00:00 2001 From: meatball Date: Thu, 30 May 2024 22:37:55 +0200 Subject: [PATCH 085/155] Make solution complaint with spec --- src/invidious/videos.cr | 2 +- src/invidious/videos/parser.cr | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index cdfca02c..5a4a55c3 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -250,7 +250,7 @@ struct Video end def genre_url : String? - info["genreUcid"].try &.as_s? ? "/channel/#{info["genreUcid"]}" : nil + info["genreUcid"]? == "" ? nil : "/channel/#{info["genreUcid"]}" end def is_vr : Bool? diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr index 4bdb2512..0e1a947c 100644 --- a/src/invidious/videos/parser.cr +++ b/src/invidious/videos/parser.cr @@ -424,7 +424,7 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any "shortDescription" => JSON::Any.new(short_description.try &.as_s || nil), # Video metadata "genre" => JSON::Any.new(genre.try &.as_s || ""), - "genreUcid" => JSON::Any.new(genre_ucid.try &.as_s?), + "genreUcid" => JSON::Any.new(genre_ucid.try &.as_s || ""), "license" => JSON::Any.new(license.try &.as_s || ""), # Music section "music" => JSON.parse(music_list.to_json), From 0224162ad22dc19d58a73202d796eb3e99f0a71c Mon Sep 17 00:00:00 2001 From: syeopite Date: Tue, 11 Jun 2024 17:57:33 -0700 Subject: [PATCH 086/155] Rewrite transcript logic to be more generic The transcript logic in Invidious was written specifically as a workaround for captions, and not transcripts as a feature. This commit genericises the logic a bit as so it can be used for implementing transcripts within Invidious' API and UI as well. The most notable change is the added parsing of section headings when it was previously skipped over in favor of regular lines. --- src/invidious/routes/api/v1/videos.cr | 9 ++- src/invidious/videos/transcript.cr | 90 +++++++++++++++++---------- 2 files changed, 63 insertions(+), 36 deletions(-) diff --git a/src/invidious/routes/api/v1/videos.cr b/src/invidious/routes/api/v1/videos.cr index 9281f4dd..faff2f59 100644 --- a/src/invidious/routes/api/v1/videos.cr +++ b/src/invidious/routes/api/v1/videos.cr @@ -89,9 +89,14 @@ 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.get_transcript(params) - webvtt = Invidious::Videos::Transcript.convert_transcripts_to_vtt(initial_data, caption.language_code) + transcript = Invidious::Videos::Transcript.from_raw( + YoutubeAPI.get_transcript(params), + caption.language_code, + caption.auto_generated + ) + + webvtt = transcript.to_vtt else # Timedtext API handling url = URI.parse("#{caption.base_url}&tlang=#{tlang}").request_target diff --git a/src/invidious/videos/transcript.cr b/src/invidious/videos/transcript.cr index dac00eea..42f29f46 100644 --- a/src/invidious/videos/transcript.cr +++ b/src/invidious/videos/transcript.cr @@ -1,8 +1,21 @@ module Invidious::Videos - # Namespace for methods primarily relating to Transcripts - module Transcript - record TranscriptLine, start_ms : Time::Span, end_ms : Time::Span, line : String + # A `Transcripts` struct encapsulates a sequence of lines that together forms the whole transcript for a given YouTube video. + # These lines can be categorized into two types: section headings and regular lines representing content from the video. + struct Transcript + # Types + record HeadingLine, start_ms : Time::Span, end_ms : Time::Span, line : String + record RegularLine, start_ms : Time::Span, end_ms : Time::Span, line : String + alias TranscriptLine = HeadingLine | RegularLine + property lines : Array(TranscriptLine) + property language_code : String + property auto_generated : Bool + + # Initializes a new Transcript struct with the contents and associated metadata describing it + def initialize(@lines : Array(TranscriptLine), @language_code : String, @auto_generated : Bool) + end + + # Generates a protobuf string to fetch the requested transcript from YouTube def self.generate_param(video_id : String, language_code : String, auto_generated : Bool) : String kind = auto_generated ? "asr" : "" @@ -30,48 +43,57 @@ module Invidious::Videos return params end - 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) + # Constructs a Transcripts struct from the initial YouTube response + def self.from_raw(initial_data : Hash(String, JSON::Any), language_code : String, auto_generated : Bool) + body = initial_data.dig("actions", 0, "updateEngagementPanelAction", "content", "transcriptRenderer", + "content", "transcriptSearchPanelRenderer", "body", "transcriptSegmentListRenderer", + "initialSegments").as_a + lines = [] of TranscriptLine + + body.each do |line| + if unpacked_line = line["transcriptSectionHeaderRenderer"]? + line_type = HeadingLine + else + unpacked_line = line["transcriptSegmentRenderer"] + line_type = RegularLine + end + + start_ms = unpacked_line["startMs"].as_s.to_i.millisecond + end_ms = unpacked_line["endMs"].as_s.to_i.millisecond + text = extract_text(unpacked_line["snippet"]) || "" + + lines << line_type.new(start_ms, end_ms, text) + end + + return Transcript.new( + lines: lines, + language_code: language_code, + auto_generated: auto_generated, + ) + end + + # Converts transcript lines to a WebVTT file + # + # This is used within Invidious to replace subtitles + # as to workaround YouTube's rate-limited timedtext endpoint. + def to_vtt settings_field = { "Kind" => "captions", - "Language" => target_language, + "Language" => @language_code, } - # Taken from Invidious::Videos::Captions::Metadata.timedtext_to_vtt() vtt = WebVTT.build(settings_field) do |vtt| - lines.each do |line| + @lines.each do |line| + # Section headers are excluded from the VTT conversion as to + # match the regular captions returned from YouTube as much as possible + next if line.is_a? HeadingLine + vtt.cue(line.start_ms, line.end_ms, line.line) end end return vtt end - - 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 - - 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 - - text = extract_text(line["snippet"]) || "" - - lines << TranscriptLine.new(start_ms, end_ms, text) - end - - return lines - end end end From 5b519123a76879edca3d5fa5cff717b58482e7e5 Mon Sep 17 00:00:00 2001 From: syeopite Date: Tue, 11 Jun 2024 18:46:34 -0700 Subject: [PATCH 087/155] Raise error when transcript does not exist --- src/invidious/videos/transcript.cr | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/invidious/videos/transcript.cr b/src/invidious/videos/transcript.cr index 42f29f46..76fb8610 100644 --- a/src/invidious/videos/transcript.cr +++ b/src/invidious/videos/transcript.cr @@ -45,13 +45,19 @@ module Invidious::Videos # Constructs a Transcripts struct from the initial YouTube response def self.from_raw(initial_data : Hash(String, JSON::Any), language_code : String, auto_generated : Bool) - body = initial_data.dig("actions", 0, "updateEngagementPanelAction", "content", "transcriptRenderer", - "content", "transcriptSearchPanelRenderer", "body", "transcriptSegmentListRenderer", - "initialSegments").as_a + segment_list = initial_data.dig("actions", 0, "updateEngagementPanelAction", "content", "transcriptRenderer", + "content", "transcriptSearchPanelRenderer", "body", "transcriptSegmentListRenderer" + ) + + if !segment_list["initialSegments"]? + raise NotFoundException.new("Requested transcript does not exist") + end + + initial_segments = segment_list["initialSegments"].as_a lines = [] of TranscriptLine - body.each do |line| + initial_segments.each do |line| if unpacked_line = line["transcriptSectionHeaderRenderer"]? line_type = HeadingLine else From f466116cd715120a8acea2c388e306caaf62abb0 Mon Sep 17 00:00:00 2001 From: syeopite Date: Thu, 13 Jun 2024 09:05:47 -0700 Subject: [PATCH 088/155] Extract label for transcript in YouTube response --- src/invidious/videos/transcript.cr | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/src/invidious/videos/transcript.cr b/src/invidious/videos/transcript.cr index 76fb8610..9cd064c5 100644 --- a/src/invidious/videos/transcript.cr +++ b/src/invidious/videos/transcript.cr @@ -8,11 +8,16 @@ module Invidious::Videos alias TranscriptLine = HeadingLine | RegularLine property lines : Array(TranscriptLine) + property language_code : String property auto_generated : Bool + # User friendly label for the current transcript. + # Example: "English (auto-generated)" + property label : String + # Initializes a new Transcript struct with the contents and associated metadata describing it - def initialize(@lines : Array(TranscriptLine), @language_code : String, @auto_generated : Bool) + def initialize(@lines : Array(TranscriptLine), @language_code : String, @auto_generated : Bool, @label : String) end # Generates a protobuf string to fetch the requested transcript from YouTube @@ -45,14 +50,29 @@ module Invidious::Videos # Constructs a Transcripts struct from the initial YouTube response def self.from_raw(initial_data : Hash(String, JSON::Any), language_code : String, auto_generated : Bool) - segment_list = initial_data.dig("actions", 0, "updateEngagementPanelAction", "content", "transcriptRenderer", - "content", "transcriptSearchPanelRenderer", "body", "transcriptSegmentListRenderer" - ) + transcript_panel = initial_data.dig("actions", 0, "updateEngagementPanelAction", "content", "transcriptRenderer", + "content", "transcriptSearchPanelRenderer") + + segment_list = transcript_panel.dig("body", "transcriptSegmentListRenderer") if !segment_list["initialSegments"]? raise NotFoundException.new("Requested transcript does not exist") end + # Extract user-friendly label for the current transcript + + footer_language_menu = transcript_panel.dig?( + "footer", "transcriptFooterRenderer", "languageMenu", "sortFilterSubMenuRenderer", "subMenuItems" + ) + + if footer_language_menu + label = footer_language_menu.as_a.select(&.["selected"].as_bool)[0]["title"].as_s + else + label = language_code + end + + # Extract transcript lines + initial_segments = segment_list["initialSegments"].as_a lines = [] of TranscriptLine @@ -76,6 +96,7 @@ module Invidious::Videos lines: lines, language_code: language_code, auto_generated: auto_generated, + label: label ) end From e82c965e897494cdb200a13407e75973f6ab03c5 Mon Sep 17 00:00:00 2001 From: Fijxu Date: Wed, 5 Jun 2024 11:26:57 -0400 Subject: [PATCH 089/155] Player: Fix video playback for videos that have already been watched. Trying to watch an already watched video will make the video start 15 seconds before the end of the video. This is not very comfortable when listening to music or watching/listening playlists over and over. --- assets/js/player.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/assets/js/player.js b/assets/js/player.js index 71c5e7da..d32062c6 100644 --- a/assets/js/player.js +++ b/assets/js/player.js @@ -351,7 +351,12 @@ if (video_data.params.save_player_pos) { const rememberedTime = get_video_time(); let lastUpdated = 0; - if(!hasTimeParam) set_seconds_after_start(rememberedTime); + if(!hasTimeParam) { + if (rememberedTime >= video_data.length_seconds - 20) + set_seconds_after_start(0); + else + set_seconds_after_start(rememberedTime); + } player.on('timeupdate', function () { const raw = player.currentTime(); From 45fd4a1968e7c19b1366eb6c05f370efbd2756cd Mon Sep 17 00:00:00 2001 From: syeopite Date: Sun, 16 Jun 2024 13:11:48 -0700 Subject: [PATCH 090/155] Add job to lint code through Ameba in CI --- .github/workflows/ci.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 057e4d61..0db0cb75 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -124,4 +124,26 @@ jobs: - name: Test Docker run: while curl -Isf http://localhost:3000; do sleep 1; done + ameba_lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - name: Install Crystal + uses: crystal-lang/install-crystal@v1.8.0 + with: + crystal: latest + + - name: Cache Shards + uses: actions/cache@v3 + with: + path: ./lib + key: shards-${{ hashFiles('shard.lock') }} + + - name: Install Shards + run: shards install + + - name: Run Ameba linter + run: bin/ameba From a644d76497ad48baf7a5d0230151fdcc4eb33414 Mon Sep 17 00:00:00 2001 From: syeopite Date: Sun, 16 Jun 2024 13:12:15 -0700 Subject: [PATCH 091/155] Update ameba config --- .ameba.yml | 58 +++++++++++------------------------------------------- 1 file changed, 11 insertions(+), 47 deletions(-) diff --git a/.ameba.yml b/.ameba.yml index 96cbc8f0..c7629dcb 100644 --- a/.ameba.yml +++ b/.ameba.yml @@ -20,6 +20,9 @@ Lint/ShadowingOuterLocalVar: Excluded: - src/invidious/helpers/tokens.cr +Lint/NotNil: + Enabled: false + # # Style @@ -31,6 +34,13 @@ Style/RedundantBegin: Style/RedundantReturn: Enabled: false +Style/ParenthesesAroundCondition: + Enabled: false + +# This requires a rewrite of most data structs (and their usage) in Invidious. +Style/QueryBoolMethods: + Enabled: false + # # Metrics @@ -39,50 +49,4 @@ Style/RedundantReturn: # Ignore function complexity (number of if/else & case/when branches) # For some functions that can hardly be simplified for now Metrics/CyclomaticComplexity: - Excluded: - # get_about_info(ucid, locale) => [17/10] - - src/invidious/channels/about.cr - - # fetch_channel_community(ucid, continuation, ...) => [34/10] - - src/invidious/channels/community.cr - - # create_notification_stream(env, topics, connection_channel) => [14/10] - - src/invidious/helpers/helpers.cr:84:5 - - # get_index(plural_form, count) => [25/10] - - src/invidious/helpers/i18next.cr - - # call(context) => [18/10] - - src/invidious/helpers/static_file_handler.cr - - # show(env) => [38/10] - - src/invidious/routes/embed.cr - - # get_video_playback(env) => [45/10] - - src/invidious/routes/video_playback.cr - - # handle(env) => [40/10] - - src/invidious/routes/watch.cr - - # playlist_ajax(env) => [24/10] - - src/invidious/routes/playlists.cr - - # fetch_youtube_comments(id, cursor, ....) => [40/10] - # template_youtube_comments(comments, locale, ...) => [16/10] - # content_to_comment_html(content) => [14/10] - - src/invidious/comments.cr - - # to_json(locale, json) => [21/10] - # extract_video_info(video_id, ...) => [44/10] - # process_video_params(query, preferences) => [20/10] - - src/invidious/videos.cr - - - -#src/invidious/playlists.cr:327:5 -#[C] Metrics/CyclomaticComplexity: Cyclomatic complexity too high [19/10] -# fetch_playlist(plid : String) - -#src/invidious/playlists.cr:436:5 -#[C] Metrics/CyclomaticComplexity: Cyclomatic complexity too high [11/10] -# extract_playlist_videos(initial_data : Hash(String, JSON::Any)) + Enabled: false From e0ed094cc46c6e3e7b37e5e3fbfc8bea9bc267a6 Mon Sep 17 00:00:00 2001 From: syeopite Date: Sun, 16 Jun 2024 13:29:06 -0700 Subject: [PATCH 092/155] Cache ameba binary --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0db0cb75..eb18f639 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -139,7 +139,9 @@ jobs: - name: Cache Shards uses: actions/cache@v3 with: - path: ./lib + path: | + ./lib + ./bin key: shards-${{ hashFiles('shard.lock') }} - name: Install Shards From 6b429575bfa8adcdf2a57a09312c7700237d8a13 Mon Sep 17 00:00:00 2001 From: syeopite Date: Sun, 16 Jun 2024 16:22:01 -0700 Subject: [PATCH 093/155] Update ameba version --- shard.lock | 2 +- shard.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/shard.lock b/shard.lock index efb60a59..397bd8bc 100644 --- a/shard.lock +++ b/shard.lock @@ -2,7 +2,7 @@ version: 2.0 shards: ameba: git: https://github.com/crystal-ameba/ameba.git - version: 1.5.0 + version: 1.6.1 athena-negotiation: git: https://github.com/athena-framework/negotiation.git diff --git a/shard.yml b/shard.yml index be06a7df..367f7c73 100644 --- a/shard.yml +++ b/shard.yml @@ -35,7 +35,7 @@ development_dependencies: version: ~> 0.10.4 ameba: github: crystal-ameba/ameba - version: ~> 1.5.0 + version: ~> 1.6.1 crystal: ">= 1.0.0, < 2.0.0" From 248df785d764023d8ffcdfa8cad08c17a12fe7a6 Mon Sep 17 00:00:00 2001 From: meatball Date: Tue, 18 Jun 2024 20:55:14 +0200 Subject: [PATCH 094/155] Update spec and rollback to last commits changes --- spec/invidious/videos/regular_videos_extract_spec.cr | 4 ++-- spec/invidious/videos/scheduled_live_extract_spec.cr | 2 +- src/invidious/videos.cr | 2 +- src/invidious/videos/parser.cr | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/invidious/videos/regular_videos_extract_spec.cr b/spec/invidious/videos/regular_videos_extract_spec.cr index a6a3e60a..b35738f4 100644 --- a/spec/invidious/videos/regular_videos_extract_spec.cr +++ b/spec/invidious/videos/regular_videos_extract_spec.cr @@ -67,7 +67,7 @@ Spectator.describe "parse_video_info" do # Video metadata expect(info["genre"].as_s).to eq("Entertainment") - expect(info["genreUcid"].as_s).to be_empty + expect(info["genreUcid"].as_s).to be_nil expect(info["license"].as_s).to be_empty # Author infos @@ -151,7 +151,7 @@ Spectator.describe "parse_video_info" do # Video metadata expect(info["genre"].as_s).to eq("Music") - expect(info["genreUcid"].as_s).to be_empty + expect(info["genreUcid"].as_s).to be_nil expect(info["license"].as_s).to be_empty # Author infos diff --git a/spec/invidious/videos/scheduled_live_extract_spec.cr b/spec/invidious/videos/scheduled_live_extract_spec.cr index 25e08c51..82dd8f00 100644 --- a/spec/invidious/videos/scheduled_live_extract_spec.cr +++ b/spec/invidious/videos/scheduled_live_extract_spec.cr @@ -94,7 +94,7 @@ Spectator.describe "parse_video_info" do # Video metadata expect(info["genre"].as_s).to eq("Entertainment") - expect(info["genreUcid"].as_s).to be_empty + expect(info["genreUcid"].as_s).to be_nil expect(info["license"].as_s).to be_empty # Author infos diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index 5a4a55c3..cdfca02c 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -250,7 +250,7 @@ struct Video end def genre_url : String? - info["genreUcid"]? == "" ? nil : "/channel/#{info["genreUcid"]}" + info["genreUcid"].try &.as_s? ? "/channel/#{info["genreUcid"]}" : nil end def is_vr : Bool? diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr index 0e1a947c..4bdb2512 100644 --- a/src/invidious/videos/parser.cr +++ b/src/invidious/videos/parser.cr @@ -424,7 +424,7 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any "shortDescription" => JSON::Any.new(short_description.try &.as_s || nil), # Video metadata "genre" => JSON::Any.new(genre.try &.as_s || ""), - "genreUcid" => JSON::Any.new(genre_ucid.try &.as_s || ""), + "genreUcid" => JSON::Any.new(genre_ucid.try &.as_s?), "license" => JSON::Any.new(license.try &.as_s || ""), # Music section "music" => JSON.parse(music_list.to_json), From 3bac467a8c25935cba801492049b0b6fe448b8a1 Mon Sep 17 00:00:00 2001 From: meatball Date: Wed, 19 Jun 2024 12:52:53 +0200 Subject: [PATCH 095/155] Call `as?` instead of `as` to not force string conversion --- spec/invidious/videos/regular_videos_extract_spec.cr | 4 ++-- spec/invidious/videos/scheduled_live_extract_spec.cr | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/invidious/videos/regular_videos_extract_spec.cr b/spec/invidious/videos/regular_videos_extract_spec.cr index b35738f4..c647c1d1 100644 --- a/spec/invidious/videos/regular_videos_extract_spec.cr +++ b/spec/invidious/videos/regular_videos_extract_spec.cr @@ -67,7 +67,7 @@ Spectator.describe "parse_video_info" do # Video metadata expect(info["genre"].as_s).to eq("Entertainment") - expect(info["genreUcid"].as_s).to be_nil + expect(info["genreUcid"].as_s?).to be_nil expect(info["license"].as_s).to be_empty # Author infos @@ -151,7 +151,7 @@ Spectator.describe "parse_video_info" do # Video metadata expect(info["genre"].as_s).to eq("Music") - expect(info["genreUcid"].as_s).to be_nil + expect(info["genreUcid"].as_s?).to be_nil expect(info["license"].as_s).to be_empty # Author infos diff --git a/spec/invidious/videos/scheduled_live_extract_spec.cr b/spec/invidious/videos/scheduled_live_extract_spec.cr index 82dd8f00..c3a9b228 100644 --- a/spec/invidious/videos/scheduled_live_extract_spec.cr +++ b/spec/invidious/videos/scheduled_live_extract_spec.cr @@ -94,7 +94,7 @@ Spectator.describe "parse_video_info" do # Video metadata expect(info["genre"].as_s).to eq("Entertainment") - expect(info["genreUcid"].as_s).to be_nil + expect(info["genreUcid"].as_s?).to be_nil expect(info["license"].as_s).to be_empty # Author infos From 933802b897bb64fec2beebabc696aba4921be68d Mon Sep 17 00:00:00 2001 From: syeopite Date: Mon, 24 Jun 2024 11:34:55 -0700 Subject: [PATCH 096/155] Use "master" label for master container build --- .github/workflows/container-release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/container-release.yml b/.github/workflows/container-release.yml index e44ac200..edb03489 100644 --- a/.github/workflows/container-release.yml +++ b/.github/workflows/container-release.yml @@ -58,7 +58,7 @@ jobs: images: quay.io/invidious/invidious tags: | type=sha,format=short,prefix={{date 'YYYY.MM.DD'}}-,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} - type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} + type=raw,value=master,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} labels: | quay.expires-after=12w @@ -83,7 +83,7 @@ jobs: suffix=-arm64 tags: | type=sha,format=short,prefix={{date 'YYYY.MM.DD'}}-,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} - type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} + type=raw,value=master,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} labels: | quay.expires-after=12w From 848ab1e9c866f8e55467697e18a4ef35503cc936 Mon Sep 17 00:00:00 2001 From: syeopite Date: Mon, 24 Jun 2024 11:36:11 -0700 Subject: [PATCH 097/155] Specify which workflow builds from master --- .github/workflows/container-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/container-release.yml b/.github/workflows/container-release.yml index edb03489..55a791b6 100644 --- a/.github/workflows/container-release.yml +++ b/.github/workflows/container-release.yml @@ -1,4 +1,4 @@ -name: Build and release container +name: Build and release container directly from master on: push: From dd38eef41aefd5dd0dc40f21489b9b6cb8269333 Mon Sep 17 00:00:00 2001 From: syeopite Date: Mon, 24 Jun 2024 11:45:00 -0700 Subject: [PATCH 098/155] Add workflow to build container on release --- .github/workflows/release-container.yml | 89 +++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 .github/workflows/release-container.yml diff --git a/.github/workflows/release-container.yml b/.github/workflows/release-container.yml new file mode 100644 index 00000000..9129c699 --- /dev/null +++ b/.github/workflows/release-container.yml @@ -0,0 +1,89 @@ +name: Build and release container + +on: + tags: + - "v*" + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Crystal + uses: crystal-lang/install-crystal@v1.8.2 + with: + crystal: 1.12.2 + + - name: Run lint + run: | + if ! crystal tool format --check; then + crystal tool format + git diff + exit 1 + fi + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + with: + platforms: arm64 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to registry + uses: docker/login-action@v3 + with: + registry: quay.io + username: ${{ secrets.QUAY_USERNAME }} + password: ${{ secrets.QUAY_PASSWORD }} + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: quay.io/invidious/invidious + tags: | + type=semvar,pattern={{version}} + type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} + labels: | + quay.expires-after=12w + + - name: Build and push Docker AMD64 image for Push Event + uses: docker/build-push-action@v5 + with: + context: . + file: docker/Dockerfile + platforms: linux/amd64 + labels: ${{ steps.meta.outputs.labels }} + push: true + tags: ${{ steps.meta.outputs.tags }} + build-args: | + "release=1" + + - name: Docker meta + id: meta-arm64 + uses: docker/metadata-action@v5 + with: + images: quay.io/invidious/invidious + flavor: | + suffix=-arm64 + tags: | + type=semvar,pattern={{version}} + type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} + labels: | + quay.expires-after=12w + + - name: Build and push Docker ARM64 image for Push Event + uses: docker/build-push-action@v5 + with: + context: . + file: docker/Dockerfile.arm64 + platforms: linux/arm64/v8 + labels: ${{ steps.meta-arm64.outputs.labels }} + push: true + tags: ${{ steps.meta-arm64.outputs.tags }} + build-args: | + "release=1" From 8f5c6a602b78e34e42d1c58ed888e9c8a70ddaa7 Mon Sep 17 00:00:00 2001 From: syeopite Date: Mon, 1 Jul 2024 21:35:08 -0700 Subject: [PATCH 099/155] Rename container workflows --- .../{container-release.yml => build-nightly-container.yml} | 0 .../{release-container.yml => build-stable-container.yml} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{container-release.yml => build-nightly-container.yml} (100%) rename .github/workflows/{release-container.yml => build-stable-container.yml} (100%) diff --git a/.github/workflows/container-release.yml b/.github/workflows/build-nightly-container.yml similarity index 100% rename from .github/workflows/container-release.yml rename to .github/workflows/build-nightly-container.yml diff --git a/.github/workflows/release-container.yml b/.github/workflows/build-stable-container.yml similarity index 100% rename from .github/workflows/release-container.yml rename to .github/workflows/build-stable-container.yml From 64d1f26eceb735ab5c96644b6545fe7fe5c2e677 Mon Sep 17 00:00:00 2001 From: syeopite Date: Mon, 1 Jul 2024 21:39:14 -0700 Subject: [PATCH 100/155] Fix trigger for stable container build --- .github/workflows/build-stable-container.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-stable-container.yml b/.github/workflows/build-stable-container.yml index 9129c699..032fd762 100644 --- a/.github/workflows/build-stable-container.yml +++ b/.github/workflows/build-stable-container.yml @@ -1,8 +1,9 @@ name: Build and release container on: - tags: - - "v*" + push: + tags: + - "v*" jobs: release: From aace30b2b47b715021a2ab661f9dec4b727604b8 Mon Sep 17 00:00:00 2001 From: syeopite Date: Thu, 4 Jul 2024 10:11:36 -0700 Subject: [PATCH 101/155] Bump nightly container build workflow crystal ver --- .github/workflows/build-nightly-container.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-nightly-container.yml b/.github/workflows/build-nightly-container.yml index 55a791b6..bee27600 100644 --- a/.github/workflows/build-nightly-container.yml +++ b/.github/workflows/build-nightly-container.yml @@ -24,9 +24,9 @@ jobs: uses: actions/checkout@v4 - name: Install Crystal - uses: crystal-lang/install-crystal@v1.8.0 + uses: crystal-lang/install-crystal@v1.8.2 with: - crystal: 1.9.2 + crystal: 1.12.2 - name: Run lint run: | From 220cc9bd2ff87872763899f730a8ba62d45db7dd Mon Sep 17 00:00:00 2001 From: syeopite Date: Thu, 4 Jul 2024 10:13:01 -0700 Subject: [PATCH 102/155] Typo Co-authored-by: Samantaz Fox --- .github/workflows/build-stable-container.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-stable-container.yml b/.github/workflows/build-stable-container.yml index 032fd762..b5fbc705 100644 --- a/.github/workflows/build-stable-container.yml +++ b/.github/workflows/build-stable-container.yml @@ -47,7 +47,7 @@ jobs: with: images: quay.io/invidious/invidious tags: | - type=semvar,pattern={{version}} + type=semver,pattern={{version}} type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} labels: | quay.expires-after=12w @@ -72,7 +72,7 @@ jobs: flavor: | suffix=-arm64 tags: | - type=semvar,pattern={{version}} + type=semver,pattern={{version}} type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} labels: | quay.expires-after=12w From 911dad69358a299b77e14303e570d48960aa0f1d Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 9 Jul 2024 13:25:18 -0400 Subject: [PATCH 103/155] Channel: parse subscriber count and channel banner --- src/invidious/channels/about.cr | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/invidious/channels/about.cr b/src/invidious/channels/about.cr index b5a27667..edaf5c12 100644 --- a/src/invidious/channels/about.cr +++ b/src/invidious/channels/about.cr @@ -72,6 +72,7 @@ def get_about_info(ucid, locale) : AboutChannel # Raises a KeyError on failure. banners = initdata["header"]["c4TabbedHeaderRenderer"]?.try &.["banner"]?.try &.["thumbnails"]? + banners ||= initdata.dig?("header", "pageHeaderRenderer", "content", "pageHeaderViewModel", "banner", "imageBannerViewModel", "image", "sources") banner = banners.try &.[-1]?.try &.["url"].as_s? # if banner.includes? "channels/c4/default_banner" @@ -147,9 +148,17 @@ def get_about_info(ucid, locale) : AboutChannel end end - sub_count = initdata - .dig?("header", "c4TabbedHeaderRenderer", "subscriberCountText", "simpleText").try &.as_s? - .try { |text| short_text_to_number(text.split(" ")[0]).to_i32 } || 0 + sub_count = 0 + + if (metadata_rows = initdata.dig?("header", "pageHeaderRenderer", "content", "pageHeaderViewModel", "metadata", "contentMetadataViewModel", "metadataRows").try &.as_a) + metadata_rows.each do |row| + metadata_part = row.dig?("metadataParts").try &.as_a.find { |i| i.dig?("text", "content").try &.as_s.includes?("subscribers") } + if !metadata_part.nil? + sub_count = short_text_to_number(metadata_part.dig("text", "content").as_s.split(" ")[0]).to_i32 + end + break if sub_count != 0 + end + end AboutChannel.new( ucid: ucid, From 593257a75025aea4df825d686de46e7f82443874 Mon Sep 17 00:00:00 2001 From: syeopite Date: Thu, 11 Jul 2024 20:45:27 -0700 Subject: [PATCH 104/155] Fix typo --- .ameba.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ameba.yml b/.ameba.yml index c7629dcb..580280cb 100644 --- a/.ameba.yml +++ b/.ameba.yml @@ -38,7 +38,7 @@ Style/ParenthesesAroundCondition: Enabled: false # This requires a rewrite of most data structs (and their usage) in Invidious. -Style/QueryBoolMethods: +Naming/QueryBoolMethods: Enabled: false From c45e71084583bcb01763a560ff83ed4afaaa7ec1 Mon Sep 17 00:00:00 2001 From: syeopite Date: Thu, 11 Jul 2024 20:47:24 -0700 Subject: [PATCH 105/155] Disable Documentation/DocumentationAdmonition rule --- .ameba.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.ameba.yml b/.ameba.yml index 580280cb..1911a47b 100644 --- a/.ameba.yml +++ b/.ameba.yml @@ -41,6 +41,13 @@ Style/ParenthesesAroundCondition: Naming/QueryBoolMethods: Enabled: false +# Hides TODO comment warnings. +# +# Call `bin/ameba --only Documentation/DocumentationAdmonition` to +# list them +Documentation/DocumentationAdmonition: + Enabled: false + # # Metrics From 8a90add3106d5dffa1bcd731a69d061844dd890f Mon Sep 17 00:00:00 2001 From: syeopite Date: Thu, 11 Jul 2024 20:53:40 -0700 Subject: [PATCH 106/155] Ameba: Fix Naming/VariableNames Fix Naming/VariableNames in comment renderer Fix Naming/VariableNames in helpers/utils Fix Naming/VariableNames in api/v1/misc.cr --- src/invidious/comments/content.cr | 36 ++++++++++++++--------------- src/invidious/helpers/utils.cr | 6 ++--- src/invidious/routes/api/v1/misc.cr | 6 ++--- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/invidious/comments/content.cr b/src/invidious/comments/content.cr index beefd9ad..3e0d41d7 100644 --- a/src/invidious/comments/content.cr +++ b/src/invidious/comments/content.cr @@ -5,35 +5,35 @@ def text_to_parsed_content(text : String) : JSON::Any # In first case line is just a simple node before # check patterns inside line # { 'text': line } - currentNodes = [] of JSON::Any - initialNode = {"text" => line} - currentNodes << (JSON.parse(initialNode.to_json)) + current_nodes = [] of JSON::Any + initial_node = {"text" => line} + current_nodes << (JSON.parse(initial_node.to_json)) # For each match with url pattern, get last node and preserve # last node before create new node with url information # { 'text': match, 'navigationEndpoint': { 'urlEndpoint' : 'url': match } } - line.scan(/https?:\/\/[^ ]*/).each do |urlMatch| + line.scan(/https?:\/\/[^ ]*/).each do |url_match| # Retrieve last node and update node without match - lastNode = currentNodes[currentNodes.size - 1].as_h - splittedLastNode = lastNode["text"].as_s.split(urlMatch[0]) - lastNode["text"] = JSON.parse(splittedLastNode[0].to_json) - currentNodes[currentNodes.size - 1] = JSON.parse(lastNode.to_json) + last_node = current_nodes[current_nodes.size - 1].as_h + splitted_last_node = last_node["text"].as_s.split(url_match[0]) + last_node["text"] = JSON.parse(splitted_last_node[0].to_json) + current_nodes[current_nodes.size - 1] = JSON.parse(last_node.to_json) # Create new node with match and navigation infos - currentNode = {"text" => urlMatch[0], "navigationEndpoint" => {"urlEndpoint" => {"url" => urlMatch[0]}}} - currentNodes << (JSON.parse(currentNode.to_json)) + current_node = {"text" => url_match[0], "navigationEndpoint" => {"urlEndpoint" => {"url" => url_match[0]}}} + current_nodes << (JSON.parse(current_node.to_json)) # If text remain after match create new simple node with text after match - afterNode = {"text" => splittedLastNode.size > 1 ? splittedLastNode[1] : ""} - currentNodes << (JSON.parse(afterNode.to_json)) + after_node = {"text" => splitted_last_node.size > 1 ? splitted_last_node[1] : ""} + current_nodes << (JSON.parse(after_node.to_json)) end # After processing of matches inside line # Add \n at end of last node for preserve carriage return - lastNode = currentNodes[currentNodes.size - 1].as_h - lastNode["text"] = JSON.parse("#{currentNodes[currentNodes.size - 1]["text"]}\n".to_json) - currentNodes[currentNodes.size - 1] = JSON.parse(lastNode.to_json) + last_node = current_nodes[current_nodes.size - 1].as_h + last_node["text"] = JSON.parse("#{current_nodes[current_nodes.size - 1]["text"]}\n".to_json) + current_nodes[current_nodes.size - 1] = JSON.parse(last_node.to_json) # Finally add final nodes to nodes returned - currentNodes.each do |node| + current_nodes.each do |node| nodes << (node) end end @@ -53,8 +53,8 @@ def content_to_comment_html(content, video_id : String? = "") text = HTML.escape(run["text"].as_s) - if navigationEndpoint = run.dig?("navigationEndpoint") - text = parse_link_endpoint(navigationEndpoint, text, video_id) + if navigation_endpoint = run.dig?("navigationEndpoint") + text = parse_link_endpoint(navigation_endpoint, text, video_id) end text = "#{text}" if run["bold"]? diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index e438e3b9..8e9e9a6a 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -52,9 +52,9 @@ def recode_length_seconds(time) end def decode_interval(string : String) : Time::Span - rawMinutes = string.try &.to_i32? + raw_minutes = string.try &.to_i32? - if !rawMinutes + if !raw_minutes hours = /(?\d+)h/.match(string).try &.["hours"].try &.to_i32 hours ||= 0 @@ -63,7 +63,7 @@ def decode_interval(string : String) : Time::Span time = Time::Span.new(hours: hours, minutes: minutes) else - time = Time::Span.new(minutes: rawMinutes) + time = Time::Span.new(minutes: raw_minutes) end return time diff --git a/src/invidious/routes/api/v1/misc.cr b/src/invidious/routes/api/v1/misc.cr index 12942906..52a985b1 100644 --- a/src/invidious/routes/api/v1/misc.cr +++ b/src/invidious/routes/api/v1/misc.cr @@ -177,8 +177,8 @@ 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 pageType == "WEB_PAGE_TYPE_UNKNOWN" + page_type = endpoint.dig?("commandMetadata", "webCommandMetadata", "webPageType").try &.as_s || "" + if page_type == "WEB_PAGE_TYPE_UNKNOWN" return error_json(400, "Unknown url") end @@ -194,7 +194,7 @@ module Invidious::Routes::API::V1::Misc json.field "playlistId", sub_endpoint["playlistId"].as_s if sub_endpoint["playlistId"]? json.field "startTimeSeconds", sub_endpoint["startTimeSeconds"].as_i if sub_endpoint["startTimeSeconds"]? json.field "params", params.try &.as_s - json.field "pageType", pageType + json.field "pageType", page_type end end end From 8d9723d43c2724df377efc65284a16faa4e08446 Mon Sep 17 00:00:00 2001 From: syeopite Date: Thu, 11 Jul 2024 21:15:45 -0700 Subject: [PATCH 107/155] Disable Naming/AccessorMethodName rule Most cases of Naming/AccessorMethodName are false positives --- .ameba.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.ameba.yml b/.ameba.yml index 1911a47b..1cd58657 100644 --- a/.ameba.yml +++ b/.ameba.yml @@ -41,6 +41,9 @@ Style/ParenthesesAroundCondition: Naming/QueryBoolMethods: Enabled: false +Naming/AccessorMethodName: + Enabled: false + # Hides TODO comment warnings. # # Call `bin/ameba --only Documentation/DocumentationAdmonition` to From 8258062ec512f9adf9523e259fbb0d33552329e9 Mon Sep 17 00:00:00 2001 From: syeopite Date: Mon, 15 Jul 2024 17:36:00 -0700 Subject: [PATCH 108/155] Ameba: Fix Lint/NotNilAfterNoBang --- src/invidious/helpers/signatures.cr | 16 ++++++++-------- src/invidious/routes/api/v1/videos.cr | 4 ++-- src/invidious/user/imports.cr | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/invidious/helpers/signatures.cr b/src/invidious/helpers/signatures.cr index ee09415b..38ded969 100644 --- a/src/invidious/helpers/signatures.cr +++ b/src/invidious/helpers/signatures.cr @@ -13,20 +13,20 @@ struct DecryptFunction private def fetch_decrypt_function(id = "CvFH_6DNRCY") document = YT_POOL.client &.get("/watch?v=#{id}&gl=US&hl=en").body - url = document.match(/src="(?\/s\/player\/[^\/]+\/player_ias[^\/]+\/en_US\/base.js)"/).not_nil!["url"] + url = document.match!(/src="(?\/s\/player\/[^\/]+\/player_ias[^\/]+\/en_US\/base.js)"/)["url"] player = YT_POOL.client &.get(url).body - function_name = player.match(/^(?[^=]+)=function\(\w\){\w=\w\.split\(""\);[^\. ]+\.[^( ]+/m).not_nil!["name"] - function_body = player.match(/^#{Regex.escape(function_name)}=function\(\w\){(?[^}]+)}/m).not_nil!["body"] + function_name = player.match!(/^(?[^=]+)=function\(\w\){\w=\w\.split\(""\);[^\. ]+\.[^( ]+/m)["name"] + function_body = player.match!(/^#{Regex.escape(function_name)}=function\(\w\){(?[^}]+)}/m)["body"] function_body = function_body.split(";")[1..-2] var_name = function_body[0][0, 2] - var_body = player.delete("\n").match(/var #{Regex.escape(var_name)}={(?(.*?))};/).not_nil!["body"] + var_body = player.delete("\n").match!(/var #{Regex.escape(var_name)}={(?(.*?))};/)["body"] operations = {} of String => SigProc var_body.split("},").each do |operation| - op_name = operation.match(/^[^:]+/).not_nil![0] - op_body = operation.match(/\{[^}]+/).not_nil![0] + op_name = operation.match!(/^[^:]+/)[0] + op_body = operation.match!(/\{[^}]+/)[0] case op_body when "{a.reverse()" @@ -42,8 +42,8 @@ struct DecryptFunction function_body.each do |function| function = function.lchop(var_name).delete("[].") - op_name = function.match(/[^\(]+/).not_nil![0] - value = function.match(/\(\w,(?[\d]+)\)/).not_nil!["value"].to_i + op_name = function.match!(/[^\(]+/)[0] + value = function.match!(/\(\w,(?[\d]+)\)/)["value"].to_i decrypt_function << {operations[op_name], value} end diff --git a/src/invidious/routes/api/v1/videos.cr b/src/invidious/routes/api/v1/videos.cr index faff2f59..4fc6a205 100644 --- a/src/invidious/routes/api/v1/videos.cr +++ b/src/invidious/routes/api/v1/videos.cr @@ -215,7 +215,7 @@ module Invidious::Routes::API::V1::Videos storyboard[:storyboard_count].times do |i| url = storyboard[:url] - authority = /(i\d?).ytimg.com/.match(url).not_nil![1]? + authority = /(i\d?).ytimg.com/.match!(url)[1]? url = url.gsub("$M", i).gsub(%r(https://i\d?.ytimg.com/sb/), "") url = "#{HOST_URL}/sb/#{authority}/#{url}" @@ -250,7 +250,7 @@ module Invidious::Routes::API::V1::Videos if CONFIG.cache_annotations && (cached_annotation = Invidious::Database::Annotations.select(id)) annotations = cached_annotation.annotations else - index = CHARS_SAFE.index(id[0]).not_nil!.to_s.rjust(2, '0') + index = CHARS_SAFE.index!(id[0]).to_s.rjust(2, '0') # IA doesn't handle leading hyphens, # so we use https://archive.org/details/youtubeannotations_64 diff --git a/src/invidious/user/imports.cr b/src/invidious/user/imports.cr index 108f2ccc..29b59293 100644 --- a/src/invidious/user/imports.cr +++ b/src/invidious/user/imports.cr @@ -182,7 +182,7 @@ struct Invidious::User if is_opml?(type, extension) subscriptions = XML.parse(body) user.subscriptions += subscriptions.xpath_nodes(%q(//outline[@type="rss"])).map do |channel| - channel["xmlUrl"].match(/UC[a-zA-Z0-9_-]{22}/).not_nil![0] + channel["xmlUrl"].match!(/UC[a-zA-Z0-9_-]{22}/)[0] end elsif extension == "json" || type == "application/json" subscriptions = JSON.parse(body) From 76ab51e219e26f118604a424d2cd62e3425786b5 Mon Sep 17 00:00:00 2001 From: syeopite Date: Wed, 17 Jul 2024 12:17:05 -0700 Subject: [PATCH 109/155] Ameba: Disable Naming/BlockParameterName --- .ameba.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.ameba.yml b/.ameba.yml index 1cd58657..39a0965e 100644 --- a/.ameba.yml +++ b/.ameba.yml @@ -44,6 +44,9 @@ Naming/QueryBoolMethods: Naming/AccessorMethodName: Enabled: false +Naming/BlockParameterName: + Enabled: false + # Hides TODO comment warnings. # # Call `bin/ameba --only Documentation/DocumentationAdmonition` to From fa50e0abf40f120a021229dfdff0d3aff7f3cfe6 Mon Sep 17 00:00:00 2001 From: syeopite Date: Wed, 17 Jul 2024 12:21:48 -0700 Subject: [PATCH 110/155] Simplify last_node retrieval Co-authored-by: Samantaz Fox --- src/invidious/comments/content.cr | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/invidious/comments/content.cr b/src/invidious/comments/content.cr index 3e0d41d7..1f55bfe6 100644 --- a/src/invidious/comments/content.cr +++ b/src/invidious/comments/content.cr @@ -14,10 +14,10 @@ def text_to_parsed_content(text : String) : JSON::Any # { 'text': match, 'navigationEndpoint': { 'urlEndpoint' : 'url': match } } line.scan(/https?:\/\/[^ ]*/).each do |url_match| # Retrieve last node and update node without match - last_node = current_nodes[current_nodes.size - 1].as_h + last_node = current_nodes[-1].as_h splitted_last_node = last_node["text"].as_s.split(url_match[0]) last_node["text"] = JSON.parse(splitted_last_node[0].to_json) - current_nodes[current_nodes.size - 1] = JSON.parse(last_node.to_json) + current_nodes[-1] = JSON.parse(last_node.to_json) # Create new node with match and navigation infos current_node = {"text" => url_match[0], "navigationEndpoint" => {"urlEndpoint" => {"url" => url_match[0]}}} current_nodes << (JSON.parse(current_node.to_json)) @@ -28,9 +28,9 @@ def text_to_parsed_content(text : String) : JSON::Any # After processing of matches inside line # Add \n at end of last node for preserve carriage return - last_node = current_nodes[current_nodes.size - 1].as_h - last_node["text"] = JSON.parse("#{current_nodes[current_nodes.size - 1]["text"]}\n".to_json) - current_nodes[current_nodes.size - 1] = JSON.parse(last_node.to_json) + last_node = current_nodes[-1].as_h + last_node["text"] = JSON.parse("#{last_node["text"]}\n".to_json) + current_nodes[-1] = JSON.parse(last_node.to_json) # Finally add final nodes to nodes returned current_nodes.each do |node| From fad0a4f52d7c9b2f9310c1c52156560ddd3f36a3 Mon Sep 17 00:00:00 2001 From: syeopite Date: Wed, 17 Jul 2024 12:39:40 -0700 Subject: [PATCH 111/155] Ameba: Fix Lint/UselessAssign --- spec/invidious/search/iv_filters_spec.cr | 1 - src/invidious/channels/channels.cr | 2 +- src/invidious/frontend/misc.cr | 4 ++-- src/invidious/helpers/handlers.cr | 2 +- src/invidious/user/imports.cr | 2 +- src/invidious/videos.cr | 4 ---- src/invidious/yt_backend/connection_pool.cr | 2 +- src/invidious/yt_backend/extractors.cr | 1 - src/invidious/yt_backend/extractors_utils.cr | 2 +- 9 files changed, 7 insertions(+), 13 deletions(-) diff --git a/spec/invidious/search/iv_filters_spec.cr b/spec/invidious/search/iv_filters_spec.cr index b0897a63..3cefafa1 100644 --- a/spec/invidious/search/iv_filters_spec.cr +++ b/spec/invidious/search/iv_filters_spec.cr @@ -301,7 +301,6 @@ Spectator.describe Invidious::Search::Filters do it "Encodes features filter (single)" do Invidious::Search::Filters::Features.each do |value| - string = described_class.format_features(value) filters = described_class.new(features: value) expect("#{filters.to_iv_params}") diff --git a/src/invidious/channels/channels.cr b/src/invidious/channels/channels.cr index be739673..29546e38 100644 --- a/src/invidious/channels/channels.cr +++ b/src/invidious/channels/channels.cr @@ -232,7 +232,7 @@ def fetch_channel(ucid, pull_all_videos : Bool) id: video_id, title: title, published: published, - updated: Time.utc, + updated: updated, ucid: ucid, author: author, length_seconds: length_seconds, diff --git a/src/invidious/frontend/misc.cr b/src/invidious/frontend/misc.cr index 43ba9f5c..7a6cf79d 100644 --- a/src/invidious/frontend/misc.cr +++ b/src/invidious/frontend/misc.cr @@ -6,9 +6,9 @@ module Invidious::Frontend::Misc if prefs.automatic_instance_redirect current_page = env.get?("current_page").as(String) - redirect_url = "/redirect?referer=#{current_page}" + return "/redirect?referer=#{current_page}" else - redirect_url = "https://redirect.invidious.io#{env.request.resource}" + return "https://redirect.invidious.io#{env.request.resource}" end end end diff --git a/src/invidious/helpers/handlers.cr b/src/invidious/helpers/handlers.cr index 174f620d..f3e3b951 100644 --- a/src/invidious/helpers/handlers.cr +++ b/src/invidious/helpers/handlers.cr @@ -97,7 +97,7 @@ class AuthHandler < Kemal::Handler if token = env.request.headers["Authorization"]? token = JSON.parse(URI.decode_www_form(token.lchop("Bearer "))) session = URI.decode_www_form(token["session"].as_s) - scopes, expire, signature = validate_request(token, session, env.request, HMAC_KEY, nil) + scopes, _, _ = validate_request(token, session, env.request, HMAC_KEY, nil) if email = Invidious::Database::SessionIDs.select_email(session) user = Invidious::Database::Users.select!(email: email) diff --git a/src/invidious/user/imports.cr b/src/invidious/user/imports.cr index 108f2ccc..4a3e1259 100644 --- a/src/invidious/user/imports.cr +++ b/src/invidious/user/imports.cr @@ -124,7 +124,7 @@ struct Invidious::User playlist = create_playlist(title, privacy, user) Invidious::Database::Playlists.update_description(playlist.id, description) - videos = item["videos"]?.try &.as_a?.try &.each_with_index do |video_id, idx| + item["videos"]?.try &.as_a?.try &.each_with_index do |video_id, idx| if idx > CONFIG.playlist_length_limit raise InfoException.new("Playlist cannot have more than #{CONFIG.playlist_length_limit} videos") end diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index c218b4ef..9a357376 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -394,10 +394,6 @@ end def fetch_video(id, region) info = extract_video_info(video_id: id) - allowed_regions = info - .dig?("microformat", "playerMicroformatRenderer", "availableCountries") - .try &.as_a.map &.as_s || [] of String - if reason = info["reason"]? if reason == "Video unavailable" raise NotFoundException.new(reason.as_s || "") diff --git a/src/invidious/yt_backend/connection_pool.cr b/src/invidious/yt_backend/connection_pool.cr index d3dbcc0e..c0356c59 100644 --- a/src/invidious/yt_backend/connection_pool.cr +++ b/src/invidious/yt_backend/connection_pool.cr @@ -24,7 +24,7 @@ struct YoutubeConnectionPool @pool = build_pool() end - def client(&block) + def client(&) conn = pool.checkout begin response = yield conn diff --git a/src/invidious/yt_backend/extractors.cr b/src/invidious/yt_backend/extractors.cr index 0e72957e..0f4f59b8 100644 --- a/src/invidious/yt_backend/extractors.cr +++ b/src/invidious/yt_backend/extractors.cr @@ -109,7 +109,6 @@ private module Parsers end live_now = false - paid = false premium = false premiere_timestamp = item_contents.dig?("upcomingEventData", "startTime").try { |t| Time.unix(t.as_s.to_i64) } diff --git a/src/invidious/yt_backend/extractors_utils.cr b/src/invidious/yt_backend/extractors_utils.cr index 11d95958..c83a2de5 100644 --- a/src/invidious/yt_backend/extractors_utils.cr +++ b/src/invidious/yt_backend/extractors_utils.cr @@ -83,5 +83,5 @@ end def extract_selected_tab(tabs) # Extract the selected tab from the array of tabs Youtube returns - return selected_target = tabs.as_a.select(&.["tabRenderer"]?.try &.["selected"]?.try &.as_bool)[0]["tabRenderer"] + return tabs.as_a.select(&.["tabRenderer"]?.try &.["selected"]?.try &.as_bool)[0]["tabRenderer"] end From 8575794bada3e1391bfe9836ab18df29135c4db1 Mon Sep 17 00:00:00 2001 From: syeopite Date: Wed, 17 Jul 2024 12:52:13 -0700 Subject: [PATCH 112/155] Exclude spec/parsers_helper from Lint/SpecFilename False positive --- .ameba.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.ameba.yml b/.ameba.yml index 39a0965e..df97b539 100644 --- a/.ameba.yml +++ b/.ameba.yml @@ -23,6 +23,10 @@ Lint/ShadowingOuterLocalVar: Lint/NotNil: Enabled: false +Lint/SpecFilename: + Excluded: + - spec/parsers_helper.cr + # # Style From 53223f99b03ac1a51cb35f7c33d4939083dc6f1a Mon Sep 17 00:00:00 2001 From: Emilien Devos <4016501+unixfox@users.noreply.github.com> Date: Wed, 24 Jul 2024 19:28:47 +0200 Subject: [PATCH 113/155] Add ability to set po_token and visitordata ID --- config/config.example.yml | 12 ++++++++++++ src/invidious/config.cr | 5 +++++ src/invidious/videos/parser.cr | 11 ++++++++--- src/invidious/yt_backend/youtube_api.cr | 11 +++++++++++ 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/config/config.example.yml b/config/config.example.yml index 38085a20..f666405e 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -173,6 +173,18 @@ https_only: false ## # use_innertube_for_captions: false +## +## Send Google session informations. This is useful when Invidious is blocked +## by the message "This helps protect our community." +## See https://github.com/iv-org/invidious/issues/4734. +## +## Warning: These strings gives much more identifiable information to Google! +## +## Accepted values: String +## Default: +## +# po_token: "" +# visitor_data: "" # ----------------------------- # Logging diff --git a/src/invidious/config.cr b/src/invidious/config.cr index 09c2168b..5340d4f5 100644 --- a/src/invidious/config.cr +++ b/src/invidious/config.cr @@ -130,6 +130,11 @@ class Config # Use Innertube's transcripts API instead of timedtext for closed captions property use_innertube_for_captions : Bool = false + # visitor data ID for Google session + property visitor_data : String? = nil + # poToken for passing bot attestation + property po_token : String? = nil + # 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/videos/parser.cr b/src/invidious/videos/parser.cr index 4bdb2512..95fa3d79 100644 --- a/src/invidious/videos/parser.cr +++ b/src/invidious/videos/parser.cr @@ -55,7 +55,7 @@ def extract_video_info(video_id : String) client_config = YoutubeAPI::ClientConfig.new # Fetch data from the player endpoint - player_response = YoutubeAPI.player(video_id: video_id, params: "", client_config: client_config) + player_response = YoutubeAPI.player(video_id: video_id, params: "2AMB", client_config: client_config) playability_status = player_response.dig?("playabilityStatus", "status").try &.as_s @@ -102,7 +102,9 @@ def extract_video_info(video_id : String) new_player_response = nil - if reason.nil? + # Don't use Android client if po_token is passed because po_token doesn't + # work for Android client. + if reason.nil? && CONFIG.po_token.nil? # Fetch the video streams using an Android client in order to get the # decrypted URLs and maybe fix throttling issues (#2194). See the # following issue for an explanation about decrypted URLs: @@ -112,7 +114,10 @@ def extract_video_info(video_id : String) end # Last hope - if new_player_response.nil? + # Only trigger if reason found and po_token or didn't work wth Android client. + # TvHtml5ScreenEmbed now requires sig helper for it to work but po_token is not required + # if the IP address is not blocked. + if CONFIG.po_token && reason || CONFIG.po_token.nil? && new_player_response.nil? client_config.client_type = YoutubeAPI::ClientType::TvHtml5ScreenEmbed new_player_response = try_fetch_streaming_data(video_id, client_config) end diff --git a/src/invidious/yt_backend/youtube_api.cr b/src/invidious/yt_backend/youtube_api.cr index c8b037c8..0efbe949 100644 --- a/src/invidious/yt_backend/youtube_api.cr +++ b/src/invidious/yt_backend/youtube_api.cr @@ -320,6 +320,10 @@ module YoutubeAPI client_context["client"]["platform"] = platform end + if CONFIG.visitor_data.is_a?(String) + client_context["client"]["visitorData"] = CONFIG.visitor_data.as(String) + end + return client_context end @@ -467,6 +471,9 @@ module YoutubeAPI "html5Preference": "HTML5_PREF_WANTS", }, }, + "serviceIntegrityDimensions" => { + "poToken" => CONFIG.po_token, + }, } # Append the additional parameters if those were provided @@ -599,6 +606,10 @@ module YoutubeAPI headers["User-Agent"] = user_agent end + if CONFIG.visitor_data.is_a?(String) + headers["X-Goog-Visitor-Id"] = CONFIG.visitor_data.as(String) + end + # Logging LOGGER.debug("YoutubeAPI: Using endpoint: \"#{endpoint}\"") LOGGER.trace("YoutubeAPI: ClientConfig: #{client_config}") From 3415507e4a9545addc21e4a985a6c0097ba9cf8b Mon Sep 17 00:00:00 2001 From: syeopite Date: Wed, 24 Jul 2024 19:48:34 -0700 Subject: [PATCH 114/155] Ameba: undo Lint/NotNilAfterNoBang in signatures.cr File is set to be removed with #4772 --- src/invidious/helpers/signatures.cr | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/invidious/helpers/signatures.cr b/src/invidious/helpers/signatures.cr index 38ded969..ee09415b 100644 --- a/src/invidious/helpers/signatures.cr +++ b/src/invidious/helpers/signatures.cr @@ -13,20 +13,20 @@ struct DecryptFunction private def fetch_decrypt_function(id = "CvFH_6DNRCY") document = YT_POOL.client &.get("/watch?v=#{id}&gl=US&hl=en").body - url = document.match!(/src="(?\/s\/player\/[^\/]+\/player_ias[^\/]+\/en_US\/base.js)"/)["url"] + url = document.match(/src="(?\/s\/player\/[^\/]+\/player_ias[^\/]+\/en_US\/base.js)"/).not_nil!["url"] player = YT_POOL.client &.get(url).body - function_name = player.match!(/^(?[^=]+)=function\(\w\){\w=\w\.split\(""\);[^\. ]+\.[^( ]+/m)["name"] - function_body = player.match!(/^#{Regex.escape(function_name)}=function\(\w\){(?[^}]+)}/m)["body"] + function_name = player.match(/^(?[^=]+)=function\(\w\){\w=\w\.split\(""\);[^\. ]+\.[^( ]+/m).not_nil!["name"] + function_body = player.match(/^#{Regex.escape(function_name)}=function\(\w\){(?[^}]+)}/m).not_nil!["body"] function_body = function_body.split(";")[1..-2] var_name = function_body[0][0, 2] - var_body = player.delete("\n").match!(/var #{Regex.escape(var_name)}={(?(.*?))};/)["body"] + var_body = player.delete("\n").match(/var #{Regex.escape(var_name)}={(?(.*?))};/).not_nil!["body"] operations = {} of String => SigProc var_body.split("},").each do |operation| - op_name = operation.match!(/^[^:]+/)[0] - op_body = operation.match!(/\{[^}]+/)[0] + op_name = operation.match(/^[^:]+/).not_nil![0] + op_body = operation.match(/\{[^}]+/).not_nil![0] case op_body when "{a.reverse()" @@ -42,8 +42,8 @@ struct DecryptFunction function_body.each do |function| function = function.lchop(var_name).delete("[].") - op_name = function.match!(/[^\(]+/)[0] - value = function.match!(/\(\w,(?[\d]+)\)/)["value"].to_i + op_name = function.match(/[^\(]+/).not_nil![0] + value = function.match(/\(\w,(?[\d]+)\)/).not_nil!["value"].to_i decrypt_function << {operations[op_name], value} end From 636a6d0be27cea0c0e255dfe2d0c367edc0a3fba Mon Sep 17 00:00:00 2001 From: syeopite Date: Wed, 24 Jul 2024 19:57:54 -0700 Subject: [PATCH 115/155] Ameba: Fix Lint/UnusedArgument --- src/invidious/routes/account.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/invidious/routes/account.cr b/src/invidious/routes/account.cr index 9d930841..dd65e7a6 100644 --- a/src/invidious/routes/account.cr +++ b/src/invidious/routes/account.cr @@ -53,7 +53,7 @@ module Invidious::Routes::Account return error_template(401, "Password is a required field") end - new_passwords = env.params.body.select { |k, v| k.match(/^new_password\[\d+\]$/) }.map { |k, v| v } + new_passwords = env.params.body.select { |k, _| k.match(/^new_password\[\d+\]$/) }.map { |_, v| v } if new_passwords.size <= 1 || new_passwords.uniq.size != 1 return error_template(400, "New passwords must match") @@ -240,7 +240,7 @@ module Invidious::Routes::Account return error_template(400, ex) end - scopes = env.params.body.select { |k, v| k.match(/^scopes\[\d+\]$/) }.map { |k, v| v } + scopes = env.params.body.select { |k, _| k.match(/^scopes\[\d+\]$/) }.map { |_, v| v } callback_url = env.params.body["callbackUrl"]? expire = env.params.body["expire"]?.try &.to_i? From c8fb75e6fd314bc1241bf256a2b897d409f79f42 Mon Sep 17 00:00:00 2001 From: syeopite Date: Wed, 24 Jul 2024 19:59:20 -0700 Subject: [PATCH 116/155] Ameba: Fix Lint/UnusedBlockArgument --- src/invidious/yt_backend/connection_pool.cr | 4 ++-- src/invidious/yt_backend/extractors.cr | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/invidious/yt_backend/connection_pool.cr b/src/invidious/yt_backend/connection_pool.cr index d3dbcc0e..0ac785e6 100644 --- a/src/invidious/yt_backend/connection_pool.cr +++ b/src/invidious/yt_backend/connection_pool.cr @@ -24,7 +24,7 @@ struct YoutubeConnectionPool @pool = build_pool() end - def client(&block) + def client(&) conn = pool.checkout begin response = yield conn @@ -69,7 +69,7 @@ def make_client(url : URI, region = nil, force_resolve : Bool = false) return client end -def make_client(url : URI, region = nil, force_resolve : Bool = false, &block) +def make_client(url : URI, region = nil, force_resolve : Bool = false, &) client = make_client(url, region, force_resolve) begin yield client diff --git a/src/invidious/yt_backend/extractors.cr b/src/invidious/yt_backend/extractors.cr index 0e72957e..57a5dc3d 100644 --- a/src/invidious/yt_backend/extractors.cr +++ b/src/invidious/yt_backend/extractors.cr @@ -856,7 +856,7 @@ end # # This function yields the container so that items can be parsed separately. # -def extract_items(initial_data : InitialData, &block) +def extract_items(initial_data : InitialData, &) 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 From 0db3b830b7d838f34710d7625d118a6aec821451 Mon Sep 17 00:00:00 2001 From: syeopite Date: Wed, 24 Jul 2024 20:03:41 -0700 Subject: [PATCH 117/155] Ameba: Fix Lint/HashDuplicatedKey --- src/invidious/helpers/i18next.cr | 1 - 1 file changed, 1 deletion(-) diff --git a/src/invidious/helpers/i18next.cr b/src/invidious/helpers/i18next.cr index 9f4077e1..04033e8c 100644 --- a/src/invidious/helpers/i18next.cr +++ b/src/invidious/helpers/i18next.cr @@ -95,7 +95,6 @@ module I18next::Plurals "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, } From 205f988491886c81f0179f08c23691201e2ae172 Mon Sep 17 00:00:00 2001 From: syeopite Date: Wed, 24 Jul 2024 20:04:44 -0700 Subject: [PATCH 118/155] Ameba: Fix Naming/MethodNames --- src/invidious/helpers/i18next.cr | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/invidious/helpers/i18next.cr b/src/invidious/helpers/i18next.cr index 04033e8c..c82a1f08 100644 --- a/src/invidious/helpers/i18next.cr +++ b/src/invidious/helpers/i18next.cr @@ -261,9 +261,9 @@ module I18next::Plurals when .special_hebrew? then return special_hebrew(count) when .special_odia? then return special_odia(count) # Mixed v3/v4 forms - when .special_spanish_italian? then return special_cldr_Spanish_Italian(count) - when .special_french_portuguese? then return special_cldr_French_Portuguese(count) - when .special_hungarian_serbian? then return special_cldr_Hungarian_Serbian(count) + when .special_spanish_italian? then return special_cldr_spanish_italian(count) + when .special_french_portuguese? then return special_cldr_french_portuguese(count) + when .special_hungarian_serbian? then return special_cldr_hungarian_serbian(count) else # default, if nothing matched above return 0_u8 @@ -534,7 +534,7 @@ module I18next::Plurals # # This rule is mostly compliant to CLDR v42 # - def self.special_cldr_Spanish_Italian(count : Int) : UInt8 + def self.special_cldr_spanish_italian(count : Int) : UInt8 return 0_u8 if (count == 1) # one return 1_u8 if (count != 0 && count % 1_000_000 == 0) # many return 2_u8 # other @@ -544,7 +544,7 @@ module I18next::Plurals # # This rule is mostly compliant to CLDR v42 # - def self.special_cldr_French_Portuguese(count : Int) : UInt8 + def self.special_cldr_french_portuguese(count : Int) : UInt8 return 0_u8 if (count == 0 || count == 1) # one return 1_u8 if (count % 1_000_000 == 0) # many return 2_u8 # other @@ -554,7 +554,7 @@ module I18next::Plurals # # This rule is mostly compliant to CLDR v42 # - def self.special_cldr_Hungarian_Serbian(count : Int) : UInt8 + def self.special_cldr_hungarian_serbian(count : Int) : UInt8 n_mod_10 = count % 10 n_mod_100 = count % 100 From 63a729998bbb4196efe9bcaedb5c58863e8f3d57 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Wed, 3 Jul 2024 21:13:29 +0200 Subject: [PATCH 119/155] Misc: Sync crystal overrides with current stdlib --- src/invidious/helpers/crystal_class_overrides.cr | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/invidious/helpers/crystal_class_overrides.cr b/src/invidious/helpers/crystal_class_overrides.cr index bf56d826..fec3f62c 100644 --- a/src/invidious/helpers/crystal_class_overrides.cr +++ b/src/invidious/helpers/crystal_class_overrides.cr @@ -3,9 +3,9 @@ # IPv6 addresses. # class TCPSocket - def initialize(host : String, port, dns_timeout = nil, connect_timeout = nil, family = Socket::Family::UNSPEC) + def initialize(host, port, dns_timeout = nil, connect_timeout = nil, blocking = false, family = Socket::Family::UNSPEC) Addrinfo.tcp(host, port, timeout: dns_timeout, family: family) do |addrinfo| - super(addrinfo.family, addrinfo.type, addrinfo.protocol) + super(addrinfo.family, addrinfo.type, addrinfo.protocol, blocking) connect(addrinfo, timeout: connect_timeout) do |error| close error @@ -26,7 +26,7 @@ class HTTP::Client end hostname = @host.starts_with?('[') && @host.ends_with?(']') ? @host[1..-2] : @host - io = TCPSocket.new hostname, @port, @dns_timeout, @connect_timeout, @family + io = TCPSocket.new hostname, @port, @dns_timeout, @connect_timeout, family: @family io.read_timeout = @read_timeout if @read_timeout io.write_timeout = @write_timeout if @write_timeout io.sync = false @@ -35,7 +35,7 @@ class HTTP::Client if tls = @tls tcp_socket = io begin - io = OpenSSL::SSL::Socket::Client.new(tcp_socket, context: tls, sync_close: true, hostname: @host) + io = OpenSSL::SSL::Socket::Client.new(tcp_socket, context: tls, sync_close: true, hostname: @host.rchop('.')) rescue exc # don't leak the TCP socket when the SSL connection failed tcp_socket.close From a845752fff1c5dd336e7e4a758691a874aa1d3ea Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Wed, 3 Jul 2024 18:24:08 +0200 Subject: [PATCH 120/155] Jobs: Remove the signature function update job --- config/config.example.yml | 15 --------------- src/invidious.cr | 4 ---- src/invidious/config.cr | 2 -- src/invidious/jobs/update_decrypt_function_job.cr | 14 -------------- 4 files changed, 35 deletions(-) delete mode 100644 src/invidious/jobs/update_decrypt_function_job.cr diff --git a/config/config.example.yml b/config/config.example.yml index 38085a20..142fdfb7 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -343,21 +343,6 @@ full_refresh: false ## feed_threads: 1 -## -## Enable/Disable the polling job that keeps the decryption -## function (for "secured" videos) up to date. -## -## Note: This part of the code generate a small amount of data every minute. -## This may not be desired if you have bandwidth limits set by your ISP. -## -## Note 2: This part of the code is currently broken, so changing -## this setting has no impact. -## -## Accepted values: true, false -## Default: false -## -#decrypt_polling: false - jobs: diff --git a/src/invidious.cr b/src/invidious.cr index e0bd0101..c667ff1a 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -164,10 +164,6 @@ if CONFIG.feed_threads > 0 end DECRYPT_FUNCTION = DecryptFunction.new(CONFIG.decrypt_polling) -if CONFIG.decrypt_polling - Invidious::Jobs.register Invidious::Jobs::UpdateDecryptFunctionJob.new -end - if CONFIG.statistics_enabled Invidious::Jobs.register Invidious::Jobs::StatisticsRefreshJob.new(PG_DB, SOFTWARE) end diff --git a/src/invidious/config.cr b/src/invidious/config.cr index 09c2168b..da911e04 100644 --- a/src/invidious/config.cr +++ b/src/invidious/config.cr @@ -74,8 +74,6 @@ class Config # Database configuration using 12-Factor "Database URL" syntax @[YAML::Field(converter: Preferences::URIConverter)] property database_url : URI = URI.parse("") - # Use polling to keep decryption function up to date - property decrypt_polling : Bool = false # Used for crawling channels: threads should check all videos uploaded by a channel property full_refresh : Bool = false diff --git a/src/invidious/jobs/update_decrypt_function_job.cr b/src/invidious/jobs/update_decrypt_function_job.cr deleted file mode 100644 index 6fa0ae1b..00000000 --- a/src/invidious/jobs/update_decrypt_function_job.cr +++ /dev/null @@ -1,14 +0,0 @@ -class Invidious::Jobs::UpdateDecryptFunctionJob < Invidious::Jobs::BaseJob - def begin - loop do - begin - DECRYPT_FUNCTION.update_decrypt_function - rescue ex - LOGGER.error("UpdateDecryptFunctionJob : #{ex.message}") - ensure - sleep 1.minute - Fiber.yield - end - end - end -end From 56a7488161428bb53d025246b9890f3f65edb3d4 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Mon, 1 Jul 2024 22:24:24 +0200 Subject: [PATCH 121/155] Helpers: Add inv_sig_helper client --- src/invidious/helpers/sig_helper.cr | 303 ++++++++++++++++++++++++++++ 1 file changed, 303 insertions(+) create mode 100644 src/invidious/helpers/sig_helper.cr diff --git a/src/invidious/helpers/sig_helper.cr b/src/invidious/helpers/sig_helper.cr new file mode 100644 index 00000000..622f0b38 --- /dev/null +++ b/src/invidious/helpers/sig_helper.cr @@ -0,0 +1,303 @@ +require "uri" +require "socket" +require "socket/tcp_socket" +require "socket/unix_socket" + +private alias NetworkEndian = IO::ByteFormat::NetworkEndian + +class Invidious::SigHelper + enum UpdateStatus + Updated + UpdateNotRequired + Error + end + + # ------------------- + # Payload types + # ------------------- + + abstract struct Payload + end + + struct StringPayload < Payload + getter value : String + + def initialize(str : String) + raise Exception.new("SigHelper: String can't be empty") if str.empty? + @value = str + end + + def self.from_io(io : IO) + size = io.read_bytes(UInt16, NetworkEndian) + if size == 0 # Error code + raise Exception.new("SigHelper: Server encountered an error") + end + + if str = io.gets(limit: size) + return self.new(str) + else + raise Exception.new("SigHelper: Can't read string from socket") + end + end + + def self.to_io(io : IO) + # `.to_u16` raises if there is an overflow during the conversion + io.write_bytes(@value.bytesize.to_u16, NetworkEndian) + io.write(@value.to_slice) + end + end + + private enum Opcode + FORCE_UPDATE = 0 + DECRYPT_N_SIGNATURE = 1 + DECRYPT_SIGNATURE = 2 + GET_SIGNATURE_TIMESTAMP = 3 + GET_PLAYER_STATUS = 4 + end + + private struct Request + def initialize(@opcode : Opcode, @payload : Payload?) + end + end + + # ---------------------- + # High-level functions + # ---------------------- + + module Client + # Forces the server to re-fetch the YouTube player, and extract the necessary + # components from it (nsig function code, sig function code, signature timestamp). + def force_update : UpdateStatus + request = Request.new(Opcode::FORCE_UPDATE, nil) + + value = send_request(request) do |io| + io.read_bytes(UInt16, NetworkEndian) + end + + case value + when 0x0000 then return UpdateStatus::Error + when 0xFFFF then return UpdateStatus::UpdateNotRequired + when 0xF44F then return UpdateStatus::Updated + else + raise Exception.new("SigHelper: Invalid status code received") + end + end + + # Decrypt a provided n signature using the server's current nsig function + # code, and return the result (or an error). + def decrypt_n_param(n : String) : String + request = Request.new(Opcode::DECRYPT_N_SIGNATURE, StringPayload.new(n)) + + n_dec = send_request(request) do |io| + StringPayload.from_io(io).string + rescue ex + LOGGER.debug(ex.message) + nil + end + + return n_dec + end + + # Decrypt a provided s signature using the server's current sig function + # code, and return the result (or an error). + def decrypt_sig(sig : String) : String? + request = Request.new(Opcode::DECRYPT_SIGNATURE, StringPayload.new(sig)) + + sig_dec = send_request(request) do |io| + StringPayload.from_io(io).string + rescue ex + LOGGER.debug(ex.message) + nil + end + + return sig_dec + end + + # Return the signature timestamp from the server's current player + def get_sts : UInt64? + request = Request.new(Opcode::GET_SIGNATURE_TIMESTAMP, nil) + + return send_request(request) do |io| + io.read_bytes(UInt64, NetworkEndian) + end + end + + # Return the signature timestamp from the server's current player + def get_player : UInt32? + request = Request.new(Opcode::GET_PLAYER_STATUS, nil) + + send_request(request) do |io| + has_player = io.read_bytes(UInt8) == 0xFF + player_version = io.read_bytes(UInt32, NetworkEndian) + end + + return has_player ? player_version : nil + end + + private def send_request(request : Request, &block : IO) + channel = Multiplexor.send(request) + data_io = channel.receive + return yield data_io + rescue ex + LOGGER.debug(ex.message) + return nil + end + end + + # --------------------- + # Low level functions + # --------------------- + + class Multiplexor + alias TransactionID = UInt32 + record Transaction, channel = ::Channel(Bytes).new + + @prng = Random.new + @mutex = Mutex.new + @queue = {} of TransactionID => Transaction + + @conn : Connection + + INSTANCE = new + + def initialize + @conn = Connection.new + listen + end + + def initialize(url : String) + @conn = Connection.new(url) + listen + end + + def listen : Nil + raise "Socket is closed" if @conn.closed? + + # TODO: reopen socket if unexpectedly closed + spawn do + loop do + receive_data + Fiber.sleep + end + end + end + + def self.send(request : Request) + transaction = Transaction.new + transaction_id = @prng.rand(TransactionID) + + # Add transaction to queue + @mutex.synchronize do + # On a 64-bits random integer, this should never happen. Though, just in case, ... + if @queue[transaction_id]? + raise Exception.new("SigHelper: Duplicate transaction ID! You got a shiny pokemon!") + end + + @queue[transaction_id] = transaction + end + + write_packet(transaction_id, request) + + return transaction.channel + end + + def receive_data : Payload + # Read a single packet from socker + transaction_id, data_io = read_packet + + # Remove transaction from queue + @mutex.synchronize do + transaction = @queue.delete(transaction_id) + end + + # Send data to the channel + transaction.channel.send(data) + end + + # Read a single packet from the socket + private def read_packet : {TransactionID, IO} + # Header + transaction_id = @conn.read_u32 + length = conn.read_u32 + + if length > 67_000 + raise Exception.new("SigHelper: Packet longer than expected (#{length})") + end + + # Payload + data_io = IO::Memory.new(1024) + IO.copy(@conn, data_io, limit: length) + + # data = Bytes.new() + # conn.read(data) + + return transaction_id, data_io + end + + # Write a single packet to the socket + private def write_packet(transaction_id : TransactionID, request : Request) + @conn.write_int(request.opcode) + @conn.write_int(transaction_id) + request.payload.to_io(@conn) + end + end + + class Connection + @socket : UNIXSocket | TCPSocket + @mutex = Mutex.new + + def initialize(host_or_path : String) + if host_or_path.empty? + host_or_path = default_path + + begin + case host_or_path + when.starts_with?('/') + @socket = UNIXSocket.new(host_or_path) + when .starts_with?("tcp://") + uri = URI.new(host_or_path) + @socket = TCPSocket.new(uri.host, uri.port) + else + uri = URI.new("tcp://#{host_or_path}") + @socket = TCPSocket.new(uri.host, uri.port) + end + + socket.sync = false + rescue ex + raise ConnectionError.new("Connection error", cause: ex) + end + end + + private default_path + return "/tmp/inv_sig_helper.sock" + end + + def closed? : Bool + return @socket.closed? + end + + def close : Nil + if @socket.closed? + raise Exception.new("SigHelper: Can't close socket, it's already closed") + else + @socket.close + end + end + + def gets(*args, **options) + @socket.gets(*args, **options) + end + + def read_bytes(*args, **options) + @socket.read_bytes(*args, **options) + end + + def write(*args, **options) + @socket.write(*args, **options) + end + + def write_bytes(*args, **options) + @socket.write_bytes(*args, **options) + end + end +end From ec8b7916fa4b90f99a880abc6f7d7e7b2ca2919b Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Wed, 3 Jul 2024 18:22:32 +0200 Subject: [PATCH 122/155] Videos: Make use of the video decoding --- src/invidious.cr | 1 - src/invidious/helpers/signatures.cr | 85 +++++++---------------------- src/invidious/videos.cr | 65 +++++++++++++++------- 3 files changed, 65 insertions(+), 86 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index c667ff1a..0c53197d 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -163,7 +163,6 @@ if CONFIG.feed_threads > 0 Invidious::Jobs.register Invidious::Jobs::RefreshFeedsJob.new(PG_DB) end -DECRYPT_FUNCTION = DecryptFunction.new(CONFIG.decrypt_polling) if CONFIG.statistics_enabled Invidious::Jobs.register Invidious::Jobs::StatisticsRefreshJob.new(PG_DB, SOFTWARE) end diff --git a/src/invidious/helpers/signatures.cr b/src/invidious/helpers/signatures.cr index ee09415b..3b5c99eb 100644 --- a/src/invidious/helpers/signatures.cr +++ b/src/invidious/helpers/signatures.cr @@ -1,73 +1,28 @@ -alias SigProc = Proc(Array(String), Int32, Array(String)) +require "http/params" +require "./sig_helper" -struct DecryptFunction - @decrypt_function = [] of {SigProc, Int32} - @decrypt_time = Time.monotonic +struct Invidious::DecryptFunction + @last_update = Time.monotonic - 42.days - def initialize(@use_polling = true) + def initialize + self.check_update end - def update_decrypt_function - @decrypt_function = fetch_decrypt_function + def check_update + now = Time.monotonic + if (now - @last_update) > 60.seconds + LOGGER.debug("Signature: Player might be outdated, updating") + Invidious::SigHelper::Client.force_update + @last_update = Time.monotonic + end end - private def fetch_decrypt_function(id = "CvFH_6DNRCY") - document = YT_POOL.client &.get("/watch?v=#{id}&gl=US&hl=en").body - url = document.match(/src="(?\/s\/player\/[^\/]+\/player_ias[^\/]+\/en_US\/base.js)"/).not_nil!["url"] - player = YT_POOL.client &.get(url).body - - function_name = player.match(/^(?[^=]+)=function\(\w\){\w=\w\.split\(""\);[^\. ]+\.[^( ]+/m).not_nil!["name"] - function_body = player.match(/^#{Regex.escape(function_name)}=function\(\w\){(?[^}]+)}/m).not_nil!["body"] - function_body = function_body.split(";")[1..-2] - - var_name = function_body[0][0, 2] - var_body = player.delete("\n").match(/var #{Regex.escape(var_name)}={(?(.*?))};/).not_nil!["body"] - - operations = {} of String => SigProc - var_body.split("},").each do |operation| - op_name = operation.match(/^[^:]+/).not_nil![0] - op_body = operation.match(/\{[^}]+/).not_nil![0] - - case op_body - when "{a.reverse()" - operations[op_name] = ->(a : Array(String), _b : Int32) { a.reverse } - when "{a.splice(0,b)" - operations[op_name] = ->(a : Array(String), b : Int32) { a.delete_at(0..(b - 1)); a } - else - operations[op_name] = ->(a : Array(String), b : Int32) { c = a[0]; a[0] = a[b % a.size]; a[b % a.size] = c; a } - end - end - - decrypt_function = [] of {SigProc, Int32} - function_body.each do |function| - function = function.lchop(var_name).delete("[].") - - op_name = function.match(/[^\(]+/).not_nil![0] - value = function.match(/\(\w,(?[\d]+)\)/).not_nil!["value"].to_i - - decrypt_function << {operations[op_name], value} - end - - return decrypt_function - end - - def decrypt_signature(fmt : Hash(String, JSON::Any)) - return "" if !fmt["s"]? || !fmt["sp"]? - - sp = fmt["sp"].as_s - sig = fmt["s"].as_s.split("") - if !@use_polling - now = Time.monotonic - if now - @decrypt_time > 60.seconds || @decrypt_function.size == 0 - @decrypt_function = fetch_decrypt_function - @decrypt_time = Time.monotonic - end - end - - @decrypt_function.each do |proc, value| - sig = proc.call(sig, value) - end - - return "&#{sp}=#{sig.join("")}" + def decrypt_signature(str : String) : String? + self.check_update + return SigHelper::Client.decrypt_sig(str) + rescue ex + LOGGER.debug(ex.message || "Signature: Unknown error") + LOGGER.trace(ex.inspect_with_backtrace) + return nil end end diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index cdfca02c..4e705556 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -1,3 +1,5 @@ +private DECRYPT_FUNCTION = IV::DecryptFunction.new + enum VideoType Video Livestream @@ -98,20 +100,47 @@ struct Video # Methods for parsing streaming data + def convert_url(fmt) + if cfr = fmt["signatureCipher"]?.try { |h| HTTP::Params.parse(h.as_s) } + sp = cfr["sp"] + url = URI.parse(cfr["url"]) + params = url.query_params + + LOGGER.debug("Videos: Decoding '#{cfr}'") + + unsig = DECRYPT_FUNCTION.decrypt_signature(cfr["s"]) + params[sp] = unsig if unsig + else + url = URI.parse(fmt["url"].as_s) + params = url.query_params + end + + n = DECRYPT_FUNCTION.decrypt_nsig(params["n"]) + params["n"] = n if n + + params["host"] = url.host.not_nil! + if region = self.info["region"]?.try &.as_s + params["region"] = region + end + + url.query_params = params + LOGGER.trace("Videos: new url is '#{url}'") + + return url.to_s + rescue ex + LOGGER.debug("Videos: Error when parsing video URL") + LOGGER.trace(ex.inspect_with_backtrace) + return "" + end + def fmt_stream return @fmt_stream.as(Array(Hash(String, JSON::Any))) if @fmt_stream - fmt_stream = info["streamingData"]?.try &.["formats"]?.try &.as_a.map &.as_h || [] of Hash(String, JSON::Any) - fmt_stream.each do |fmt| - if s = (fmt["cipher"]? || fmt["signatureCipher"]?).try { |h| HTTP::Params.parse(h.as_s) } - s.each do |k, v| - fmt[k] = JSON::Any.new(v) - end - fmt["url"] = JSON::Any.new("#{fmt["url"]}#{DECRYPT_FUNCTION.decrypt_signature(fmt)}") - end + fmt_stream = info.dig?("streamingData", "formats") + .try &.as_a.map &.as_h || [] of Hash(String, JSON::Any) - fmt["url"] = JSON::Any.new("#{fmt["url"]}&host=#{URI.parse(fmt["url"].as_s).host}") - fmt["url"] = JSON::Any.new("#{fmt["url"]}®ion=#{self.info["region"]}") if self.info["region"]? + fmt_stream.each do |fmt| + fmt["url"] = JSON::Any.new(self.convert_url(fmt)) end fmt_stream.sort_by! { |f| f["width"]?.try &.as_i || 0 } @@ -121,21 +150,17 @@ struct Video def adaptive_fmts return @adaptive_fmts.as(Array(Hash(String, JSON::Any))) if @adaptive_fmts - fmt_stream = info["streamingData"]?.try &.["adaptiveFormats"]?.try &.as_a.map &.as_h || [] of Hash(String, JSON::Any) - fmt_stream.each do |fmt| - if s = (fmt["cipher"]? || fmt["signatureCipher"]?).try { |h| HTTP::Params.parse(h.as_s) } - s.each do |k, v| - fmt[k] = JSON::Any.new(v) - end - fmt["url"] = JSON::Any.new("#{fmt["url"]}#{DECRYPT_FUNCTION.decrypt_signature(fmt)}") - end - fmt["url"] = JSON::Any.new("#{fmt["url"]}&host=#{URI.parse(fmt["url"].as_s).host}") - fmt["url"] = JSON::Any.new("#{fmt["url"]}®ion=#{self.info["region"]}") if self.info["region"]? + fmt_stream = info.dig("streamingData", "adaptiveFormats") + .try &.as_a.map &.as_h || [] of Hash(String, JSON::Any) + + fmt_stream.each do |fmt| + fmt["url"] = JSON::Any.new(self.convert_url(fmt)) end fmt_stream.sort_by! { |f| f["width"]?.try &.as_i || 0 } @adaptive_fmts = fmt_stream + return @adaptive_fmts.as(Array(Hash(String, JSON::Any))) end From b509aa91d5c0955deb4980cd08a93e8d808ee456 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Wed, 3 Jul 2024 18:20:35 +0200 Subject: [PATCH 123/155] SigHelper: Fix many issues --- src/invidious/helpers/sig_helper.cr | 226 +++++++++++++++------------- src/invidious/helpers/signatures.cr | 9 ++ 2 files changed, 133 insertions(+), 102 deletions(-) diff --git a/src/invidious/helpers/sig_helper.cr b/src/invidious/helpers/sig_helper.cr index 622f0b38..b8b985d5 100644 --- a/src/invidious/helpers/sig_helper.cr +++ b/src/invidious/helpers/sig_helper.cr @@ -3,6 +3,10 @@ require "socket" require "socket/tcp_socket" require "socket/unix_socket" +{% if flag?(:advanced_debug) %} + require "io/hexdump" +{% end %} + private alias NetworkEndian = IO::ByteFormat::NetworkEndian class Invidious::SigHelper @@ -20,58 +24,63 @@ class Invidious::SigHelper end struct StringPayload < Payload - getter value : String + getter string : String def initialize(str : String) raise Exception.new("SigHelper: String can't be empty") if str.empty? - @value = str + @string = str end - def self.from_io(io : IO) - size = io.read_bytes(UInt16, NetworkEndian) + def self.from_bytes(slice : Bytes) + size = IO::ByteFormat::NetworkEndian.decode(UInt16, slice) if size == 0 # Error code raise Exception.new("SigHelper: Server encountered an error") end - if str = io.gets(limit: size) + if (slice.bytesize - 2) != size + raise Exception.new("SigHelper: String size mismatch") + end + + if str = String.new(slice[2..]) return self.new(str) else raise Exception.new("SigHelper: Can't read string from socket") end end - def self.to_io(io : IO) + def to_io(io) # `.to_u16` raises if there is an overflow during the conversion - io.write_bytes(@value.bytesize.to_u16, NetworkEndian) - io.write(@value.to_slice) + io.write_bytes(@string.bytesize.to_u16, NetworkEndian) + io.write(@string.to_slice) end end private enum Opcode - FORCE_UPDATE = 0 - DECRYPT_N_SIGNATURE = 1 - DECRYPT_SIGNATURE = 2 + FORCE_UPDATE = 0 + DECRYPT_N_SIGNATURE = 1 + DECRYPT_SIGNATURE = 2 GET_SIGNATURE_TIMESTAMP = 3 - GET_PLAYER_STATUS = 4 + GET_PLAYER_STATUS = 4 end - private struct Request - def initialize(@opcode : Opcode, @payload : Payload?) - end - end + private record Request, + opcode : Opcode, + payload : Payload? # ---------------------- # High-level functions # ---------------------- module Client + extend self + # Forces the server to re-fetch the YouTube player, and extract the necessary # components from it (nsig function code, sig function code, signature timestamp). def force_update : UpdateStatus request = Request.new(Opcode::FORCE_UPDATE, nil) - value = send_request(request) do |io| - io.read_bytes(UInt16, NetworkEndian) + value = send_request(request) do |bytes| + IO::ByteFormat::NetworkEndian.decode(UInt16, bytes) end case value @@ -79,20 +88,18 @@ class Invidious::SigHelper when 0xFFFF then return UpdateStatus::UpdateNotRequired when 0xF44F then return UpdateStatus::Updated else - raise Exception.new("SigHelper: Invalid status code received") + code = value.nil? ? "nil" : value.to_s(base: 16) + raise Exception.new("SigHelper: Invalid status code received #{code}") end end # Decrypt a provided n signature using the server's current nsig function # code, and return the result (or an error). - def decrypt_n_param(n : String) : String + def decrypt_n_param(n : String) : String? request = Request.new(Opcode::DECRYPT_N_SIGNATURE, StringPayload.new(n)) - n_dec = send_request(request) do |io| - StringPayload.from_io(io).string - rescue ex - LOGGER.debug(ex.message) - nil + n_dec = send_request(request) do |bytes| + StringPayload.from_bytes(bytes).string end return n_dec @@ -103,11 +110,8 @@ class Invidious::SigHelper def decrypt_sig(sig : String) : String? request = Request.new(Opcode::DECRYPT_SIGNATURE, StringPayload.new(sig)) - sig_dec = send_request(request) do |io| - StringPayload.from_io(io).string - rescue ex - LOGGER.debug(ex.message) - nil + sig_dec = send_request(request) do |bytes| + StringPayload.from_bytes(bytes).string end return sig_dec @@ -117,29 +121,30 @@ class Invidious::SigHelper def get_sts : UInt64? request = Request.new(Opcode::GET_SIGNATURE_TIMESTAMP, nil) - return send_request(request) do |io| - io.read_bytes(UInt64, NetworkEndian) + return send_request(request) do |bytes| + IO::ByteFormat::NetworkEndian.decode(UInt64, bytes) end end - # Return the signature timestamp from the server's current player + # Return the current player's version def get_player : UInt32? request = Request.new(Opcode::GET_PLAYER_STATUS, nil) - send_request(request) do |io| - has_player = io.read_bytes(UInt8) == 0xFF - player_version = io.read_bytes(UInt32, NetworkEndian) + send_request(request) do |bytes| + has_player = (bytes[0] == 0xFF) + player_version = IO::ByteFormat::NetworkEndian.decode(UInt32, bytes[1..4]) end return has_player ? player_version : nil end - private def send_request(request : Request, &block : IO) - channel = Multiplexor.send(request) - data_io = channel.receive - return yield data_io + private def send_request(request : Request, &) + channel = Multiplexor::INSTANCE.send(request) + slice = channel.receive + return yield slice rescue ex - LOGGER.debug(ex.message) + LOGGER.debug("SigHelper: Error when sending a request") + LOGGER.trace(ex.inspect_with_backtrace) return nil end end @@ -152,18 +157,13 @@ class Invidious::SigHelper alias TransactionID = UInt32 record Transaction, channel = ::Channel(Bytes).new - @prng = Random.new + @prng = Random.new @mutex = Mutex.new @queue = {} of TransactionID => Transaction @conn : Connection - INSTANCE = new - - def initialize - @conn = Connection.new - listen - end + INSTANCE = new("") def initialize(url : String) @conn = Connection.new(url) @@ -173,22 +173,24 @@ class Invidious::SigHelper def listen : Nil raise "Socket is closed" if @conn.closed? + LOGGER.debug("SigHelper: Multiplexor listening") + # TODO: reopen socket if unexpectedly closed spawn do loop do receive_data - Fiber.sleep + Fiber.yield end end end - def self.send(request : Request) + def send(request : Request) transaction = Transaction.new transaction_id = @prng.rand(TransactionID) # Add transaction to queue @mutex.synchronize do - # On a 64-bits random integer, this should never happen. Though, just in case, ... + # On a 32-bits random integer, this should never happen. Though, just in case, ... if @queue[transaction_id]? raise Exception.new("SigHelper: Duplicate transaction ID! You got a shiny pokemon!") end @@ -201,75 +203,92 @@ class Invidious::SigHelper return transaction.channel end - def receive_data : Payload - # Read a single packet from socker - transaction_id, data_io = read_packet + def receive_data + transaction_id, slice = read_packet - # Remove transaction from queue @mutex.synchronize do - transaction = @queue.delete(transaction_id) + if transaction = @queue.delete(transaction_id) + # Remove transaction from queue and send data to the channel + transaction.channel.send(slice) + LOGGER.trace("SigHelper: Transaction unqueued and data sent to channel") + else + raise Exception.new("SigHelper: Received transaction was not in queue") + end end - - # Send data to the channel - transaction.channel.send(data) end # Read a single packet from the socket - private def read_packet : {TransactionID, IO} + private def read_packet : {TransactionID, Bytes} # Header - transaction_id = @conn.read_u32 - length = conn.read_u32 + transaction_id = @conn.read_bytes(UInt32, NetworkEndian) + length = @conn.read_bytes(UInt32, NetworkEndian) + + LOGGER.trace("SigHelper: Recv transaction 0x#{transaction_id.to_s(base: 16)} / length #{length}") if length > 67_000 raise Exception.new("SigHelper: Packet longer than expected (#{length})") end # Payload - data_io = IO::Memory.new(1024) - IO.copy(@conn, data_io, limit: length) + slice = Bytes.new(length) + @conn.read(slice) if length > 0 - # data = Bytes.new() - # conn.read(data) + LOGGER.trace("SigHelper: payload = #{slice}") + LOGGER.trace("SigHelper: Recv transaction 0x#{transaction_id.to_s(base: 16)} - Done") - return transaction_id, data_io + return transaction_id, slice end # Write a single packet to the socket private def write_packet(transaction_id : TransactionID, request : Request) - @conn.write_int(request.opcode) - @conn.write_int(transaction_id) - request.payload.to_io(@conn) + LOGGER.trace("SigHelper: Send transaction 0x#{transaction_id.to_s(base: 16)} / opcode #{request.opcode}") + + io = IO::Memory.new(1024) + io.write_bytes(request.opcode.to_u8, NetworkEndian) + io.write_bytes(transaction_id, NetworkEndian) + + if payload = request.payload + payload.to_io(io) + end + + @conn.send(io) + @conn.flush + + LOGGER.trace("SigHelper: Send transaction 0x#{transaction_id.to_s(base: 16)} - Done") end end class Connection @socket : UNIXSocket | TCPSocket - @mutex = Mutex.new + + {% if flag?(:advanced_debug) %} + @io : IO::Hexdump + {% end %} def initialize(host_or_path : String) if host_or_path.empty? - host_or_path = default_path - - begin - case host_or_path - when.starts_with?('/') - @socket = UNIXSocket.new(host_or_path) - when .starts_with?("tcp://") - uri = URI.new(host_or_path) - @socket = TCPSocket.new(uri.host, uri.port) - else - uri = URI.new("tcp://#{host_or_path}") - @socket = TCPSocket.new(uri.host, uri.port) - end - - socket.sync = false - rescue ex - raise ConnectionError.new("Connection error", cause: ex) + host_or_path = "/tmp/inv_sig_helper.sock" end - end - private default_path - return "/tmp/inv_sig_helper.sock" + case host_or_path + when .starts_with?('/') + @socket = UNIXSocket.new(host_or_path) + when .starts_with?("tcp://") + uri = URI.new(host_or_path) + @socket = TCPSocket.new(uri.host.not_nil!, uri.port.not_nil!) + else + uri = URI.new("tcp://#{host_or_path}") + @socket = TCPSocket.new(uri.host.not_nil!, uri.port.not_nil!) + end + + LOGGER.debug("SigHelper: Listening on '#{host_or_path}'") + + {% if flag?(:advanced_debug) %} + @io = IO::Hexdump.new(@socket, output: STDERR, read: true, write: true) + {% end %} + + @socket.sync = false + @socket.blocking = false end def closed? : Bool @@ -284,20 +303,23 @@ class Invidious::SigHelper end end - def gets(*args, **options) - @socket.gets(*args, **options) + def flush(*args, **options) + @socket.flush(*args, **options) end - def read_bytes(*args, **options) - @socket.read_bytes(*args, **options) + def send(*args, **options) + @socket.send(*args, **options) end - def write(*args, **options) - @socket.write(*args, **options) - end - - def write_bytes(*args, **options) - @socket.write_bytes(*args, **options) - end + # Wrap IO functions, with added debug tooling if needed + {% for function in %w(read read_bytes write write_bytes) %} + def {{function.id}}(*args, **options) + {% if flag?(:advanced_debug) %} + @io.{{function.id}}(*args, **options) + {% else %} + @socket.{{function.id}}(*args, **options) + {% end %} + end + {% end %} end end diff --git a/src/invidious/helpers/signatures.cr b/src/invidious/helpers/signatures.cr index 3b5c99eb..d9aab31c 100644 --- a/src/invidious/helpers/signatures.cr +++ b/src/invidious/helpers/signatures.cr @@ -17,6 +17,15 @@ struct Invidious::DecryptFunction end end + def decrypt_nsig(n : String) : String? + self.check_update + return SigHelper::Client.decrypt_n_param(n) + rescue ex + LOGGER.debug(ex.message || "Signature: Unknown error") + LOGGER.trace(ex.inspect_with_backtrace) + return nil + end + def decrypt_signature(str : String) : String? self.check_update return SigHelper::Client.decrypt_sig(str) From 10e5788c212587b7c929c84580aea3e93b2f28ea Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Wed, 3 Jul 2024 21:15:13 +0200 Subject: [PATCH 124/155] Videos: Send player sts when required --- src/invidious/helpers/signatures.cr | 9 +++++++++ src/invidious/yt_backend/youtube_api.cr | 24 ++++++++++++++++++------ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/invidious/helpers/signatures.cr b/src/invidious/helpers/signatures.cr index d9aab31c..b58af73f 100644 --- a/src/invidious/helpers/signatures.cr +++ b/src/invidious/helpers/signatures.cr @@ -34,4 +34,13 @@ struct Invidious::DecryptFunction LOGGER.trace(ex.inspect_with_backtrace) return nil end + + def get_sts : UInt64? + self.check_update + return SigHelper::Client.get_sts + rescue ex + LOGGER.debug(ex.message || "Signature: Unknown error") + LOGGER.trace(ex.inspect_with_backtrace) + return nil + end end diff --git a/src/invidious/yt_backend/youtube_api.cr b/src/invidious/yt_backend/youtube_api.cr index c8b037c8..f4ee35e5 100644 --- a/src/invidious/yt_backend/youtube_api.cr +++ b/src/invidious/yt_backend/youtube_api.cr @@ -2,6 +2,8 @@ # This file contains youtube API wrappers # +private STS_FETCHER = IV::DecryptFunction.new + module YoutubeAPI extend self @@ -272,7 +274,7 @@ module YoutubeAPI # Return, as a Hash, the "context" data required to request the # youtube API endpoints. # - private def make_context(client_config : ClientConfig | Nil) : Hash + private def make_context(client_config : ClientConfig | Nil, video_id = "dQw4w9WgXcQ") : Hash # Use the default client config if nil is passed client_config ||= DEFAULT_CLIENT_CONFIG @@ -292,7 +294,7 @@ module YoutubeAPI if client_config.screen == "EMBED" client_context["thirdParty"] = { - "embedUrl" => "https://www.youtube.com/embed/dQw4w9WgXcQ", + "embedUrl" => "https://www.youtube.com/embed/#{video_id}", } of String => String | Int64 end @@ -453,19 +455,29 @@ module YoutubeAPI params : String, client_config : ClientConfig | Nil = nil ) + # Playback context, separate because it can be different between clients + playback_ctx = { + "html5Preference" => "HTML5_PREF_WANTS", + "referer" => "https://www.youtube.com/watch?v=#{video_id}", + } of String => String | Int64 + + if {"WEB", "TVHTML5"}.any? { |s| client_config.name.starts_with? s } + if sts = STS_FETCHER.get_sts + playback_ctx["signatureTimestamp"] = sts.to_i64 + end + end + # JSON Request data, required by the API data = { "contentCheckOk" => true, "videoId" => video_id, - "context" => self.make_context(client_config), + "context" => self.make_context(client_config, video_id), "racyCheckOk" => true, "user" => { "lockedSafetyMode" => false, }, "playbackContext" => { - "contentPlaybackContext" => { - "html5Preference": "HTML5_PREF_WANTS", - }, + "contentPlaybackContext" => playback_ctx, }, } From 61d75050e46e5318a1271c2eade29469c8c9e8a5 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Thu, 4 Jul 2024 15:47:19 +0000 Subject: [PATCH 125/155] SigHelper: Use 'URI.parse' instead of 'URI.new' Co-authored-by: Brahim Hadriche --- src/invidious/helpers/sig_helper.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/invidious/helpers/sig_helper.cr b/src/invidious/helpers/sig_helper.cr index b8b985d5..09079850 100644 --- a/src/invidious/helpers/sig_helper.cr +++ b/src/invidious/helpers/sig_helper.cr @@ -274,10 +274,10 @@ class Invidious::SigHelper when .starts_with?('/') @socket = UNIXSocket.new(host_or_path) when .starts_with?("tcp://") - uri = URI.new(host_or_path) + uri = URI.parse(host_or_path) @socket = TCPSocket.new(uri.host.not_nil!, uri.port.not_nil!) else - uri = URI.new("tcp://#{host_or_path}") + uri = URI.parse("tcp://#{host_or_path}") @socket = TCPSocket.new(uri.host.not_nil!, uri.port.not_nil!) end From 6506b8dbfce93f9761999b8d91b182350b64b0ff Mon Sep 17 00:00:00 2001 From: syeopite Date: Thu, 25 Jul 2024 20:08:26 -0700 Subject: [PATCH 126/155] Ameba: Fix Naming/PredicateName --- src/invidious/helpers/i18next.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/invidious/helpers/i18next.cr b/src/invidious/helpers/i18next.cr index c82a1f08..684e6d14 100644 --- a/src/invidious/helpers/i18next.cr +++ b/src/invidious/helpers/i18next.cr @@ -188,7 +188,7 @@ module I18next::Plurals # Emulate the `rule.numbers.size == 2 && rule.numbers[0] == 1` check # from original i18next code - private def is_simple_plural(form : PluralForms) : Bool + private def simple_plural?(form : PluralForms) : Bool case form when .single_gt_one? then return true when .single_not_one? then return true @@ -210,7 +210,7 @@ module I18next::Plurals idx = SuffixIndex.get_index(plural_form, count) # Simple plurals are handled differently in all versions (but v4) - if @simplify_plural_suffix && is_simple_plural(plural_form) + if @simplify_plural_suffix && simple_plural?(plural_form) return (idx == 1) ? "_plural" : "" end From e098c27a4564f936443f298cb59ea63a49b0c118 Mon Sep 17 00:00:00 2001 From: syeopite Date: Sun, 28 Jul 2024 16:44:30 -0700 Subject: [PATCH 127/155] Remove unused methods in `Invidious::LogHandler` --- src/invidious/helpers/logger.cr | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/invidious/helpers/logger.cr b/src/invidious/helpers/logger.cr index e2e50905..b443073e 100644 --- a/src/invidious/helpers/logger.cr +++ b/src/invidious/helpers/logger.cr @@ -34,24 +34,11 @@ class Invidious::LogHandler < Kemal::BaseLogHandler context end - def puts(message : String) - @io << message << '\n' - @io.flush - end - def write(message : String) @io << message @io.flush end - def set_log_level(level : String) - @level = LogLevel.parse(level) - end - - def set_log_level(level : LogLevel) - @level = level - end - {% for level in %w(trace debug info warn error fatal) %} def {{level.id}}(message : String) if LogLevel::{{level.id.capitalize}} >= @level From 3b7e45b7bc5798e05d49658428b49536d20e745c Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Wed, 31 Jul 2024 12:17:47 +0200 Subject: [PATCH 128/155] SigHelper: Small fixes + suggestions from code review --- src/invidious/helpers/sig_helper.cr | 23 +++++++++-------------- src/invidious/helpers/signatures.cr | 2 +- src/invidious/videos.cr | 2 +- 3 files changed, 11 insertions(+), 16 deletions(-) diff --git a/src/invidious/helpers/sig_helper.cr b/src/invidious/helpers/sig_helper.cr index 09079850..108587ce 100644 --- a/src/invidious/helpers/sig_helper.cr +++ b/src/invidious/helpers/sig_helper.cr @@ -9,7 +9,7 @@ require "socket/unix_socket" private alias NetworkEndian = IO::ByteFormat::NetworkEndian -class Invidious::SigHelper +module Invidious::SigHelper enum UpdateStatus Updated UpdateNotRequired @@ -98,7 +98,7 @@ class Invidious::SigHelper def decrypt_n_param(n : String) : String? request = Request.new(Opcode::DECRYPT_N_SIGNATURE, StringPayload.new(n)) - n_dec = send_request(request) do |bytes| + n_dec = self.send_request(request) do |bytes| StringPayload.from_bytes(bytes).string end @@ -110,7 +110,7 @@ class Invidious::SigHelper def decrypt_sig(sig : String) : String? request = Request.new(Opcode::DECRYPT_SIGNATURE, StringPayload.new(sig)) - sig_dec = send_request(request) do |bytes| + sig_dec = self.send_request(request) do |bytes| StringPayload.from_bytes(bytes).string end @@ -118,10 +118,10 @@ class Invidious::SigHelper end # Return the signature timestamp from the server's current player - def get_sts : UInt64? + def get_signature_timestamp : UInt64? request = Request.new(Opcode::GET_SIGNATURE_TIMESTAMP, nil) - return send_request(request) do |bytes| + return self.send_request(request) do |bytes| IO::ByteFormat::NetworkEndian.decode(UInt64, bytes) end end @@ -130,12 +130,12 @@ class Invidious::SigHelper def get_player : UInt32? request = Request.new(Opcode::GET_PLAYER_STATUS, nil) - send_request(request) do |bytes| + return self.send_request(request) do |bytes| has_player = (bytes[0] == 0xFF) player_version = IO::ByteFormat::NetworkEndian.decode(UInt32, bytes[1..4]) + has_player ? player_version : nil end - return has_player ? player_version : nil end private def send_request(request : Request, &) @@ -280,8 +280,7 @@ class Invidious::SigHelper uri = URI.parse("tcp://#{host_or_path}") @socket = TCPSocket.new(uri.host.not_nil!, uri.port.not_nil!) end - - LOGGER.debug("SigHelper: Listening on '#{host_or_path}'") + LOGGER.info("SigHelper: Using helper at '#{host_or_path}'") {% if flag?(:advanced_debug) %} @io = IO::Hexdump.new(@socket, output: STDERR, read: true, write: true) @@ -296,11 +295,7 @@ class Invidious::SigHelper end def close : Nil - if @socket.closed? - raise Exception.new("SigHelper: Can't close socket, it's already closed") - else - @socket.close - end + @socket.close if !@socket.closed? end def flush(*args, **options) diff --git a/src/invidious/helpers/signatures.cr b/src/invidious/helpers/signatures.cr index b58af73f..8fbfaac0 100644 --- a/src/invidious/helpers/signatures.cr +++ b/src/invidious/helpers/signatures.cr @@ -37,7 +37,7 @@ struct Invidious::DecryptFunction def get_sts : UInt64? self.check_update - return SigHelper::Client.get_sts + return SigHelper::Client.get_signature_timestamp rescue ex LOGGER.debug(ex.message || "Signature: Unknown error") LOGGER.trace(ex.inspect_with_backtrace) diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index 4e705556..ed172878 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -101,7 +101,7 @@ struct Video # Methods for parsing streaming data def convert_url(fmt) - if cfr = fmt["signatureCipher"]?.try { |h| HTTP::Params.parse(h.as_s) } + if cfr = fmt["signatureCipher"]?.try { |json| HTTP::Params.parse(json.as_s) } sp = cfr["sp"] url = URI.parse(cfr["url"]) params = url.query_params From ec1bb5db87a40d74203a09ca401d0f70d0ad962d Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Thu, 1 Aug 2024 23:28:30 +0200 Subject: [PATCH 129/155] SigHelper: Add support for PLAYER_UPDATE_TIMESTAMP opcode --- config/config.example.yml | 15 ++++++++++++++- src/invidious/helpers/sig_helper.cr | 9 +++++++++ src/invidious/helpers/signatures.cr | 17 +++++++++++++---- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/config/config.example.yml b/config/config.example.yml index 142fdfb7..2f5228a6 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -1,6 +1,6 @@ ######################################### # -# Database configuration +# Database and other external servers # ######################################### @@ -41,6 +41,19 @@ db: #check_tables: false +## +## Path to an external signature resolver, used to emulate +## the Youtube client's Javascript. If no such server is +## available, some videos will not be playable. +## +## When this setting is commented out, no external +## resolver will be used. +## +## Accepted values: a path to a UNIX socket or ":" +## Default: +## +#signature_server: + ######################################### # diff --git a/src/invidious/helpers/sig_helper.cr b/src/invidious/helpers/sig_helper.cr index 108587ce..2239858b 100644 --- a/src/invidious/helpers/sig_helper.cr +++ b/src/invidious/helpers/sig_helper.cr @@ -61,6 +61,7 @@ module Invidious::SigHelper DECRYPT_SIGNATURE = 2 GET_SIGNATURE_TIMESTAMP = 3 GET_PLAYER_STATUS = 4 + PLAYER_UPDATE_TIMESTAMP = 5 end private record Request, @@ -135,7 +136,15 @@ module Invidious::SigHelper player_version = IO::ByteFormat::NetworkEndian.decode(UInt32, bytes[1..4]) has_player ? player_version : nil end + end + # Return when the player was last updated + def get_player_timestamp : UInt64? + request = Request.new(Opcode::GET_SIGNATURE_TIMESTAMP, nil) + + return self.send_request(request) do |bytes| + IO::ByteFormat::NetworkEndian.decode(UInt64, bytes) + end end private def send_request(request : Request, &) diff --git a/src/invidious/helpers/signatures.cr b/src/invidious/helpers/signatures.cr index 8fbfaac0..cf170668 100644 --- a/src/invidious/helpers/signatures.cr +++ b/src/invidious/helpers/signatures.cr @@ -2,18 +2,27 @@ require "http/params" require "./sig_helper" struct Invidious::DecryptFunction - @last_update = Time.monotonic - 42.days + @last_update : Time = Time.utc - 42.days def initialize self.check_update end def check_update - now = Time.monotonic - if (now - @last_update) > 60.seconds + now = Time.utc + + # If we have updated in the last 5 minutes, do nothing + return if (now - @last_update) > 5.minutes + + # Get the time when the player was updated, in the event where + # multiple invidious processes are run in parallel. + player_ts = Invidious::SigHelper::Client.get_player_timestamp + player_time = Time.unix(player_ts || 0) + + if (now - player_time) > 5.minutes LOGGER.debug("Signature: Player might be outdated, updating") Invidious::SigHelper::Client.force_update - @last_update = Time.monotonic + @last_update = Time.utc end end From 7798faf23425f11cee77742629ca589a5f33392b Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Wed, 7 Aug 2024 23:12:27 +0200 Subject: [PATCH 130/155] SigHelper: Make signature server optional and configurable --- src/invidious.cr | 9 +++++++++ src/invidious/config.cr | 4 ++++ src/invidious/helpers/sig_helper.cr | 27 ++++++++++++++----------- src/invidious/helpers/signatures.cr | 16 +++++++-------- src/invidious/videos.cr | 6 ++---- src/invidious/yt_backend/youtube_api.cr | 4 +--- 6 files changed, 39 insertions(+), 27 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index 0c53197d..3804197e 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -153,6 +153,15 @@ Invidious::Database.check_integrity(CONFIG) {% puts "\nDone checking player dependencies, now compiling Invidious...\n" %} {% end %} +# Misc + +DECRYPT_FUNCTION = + if sig_helper_address = CONFIG.signature_server.presence + IV::DecryptFunction.new(sig_helper_address) + else + nil + end + # Start jobs if CONFIG.channel_threads > 0 diff --git a/src/invidious/config.cr b/src/invidious/config.cr index da911e04..29c39bd6 100644 --- a/src/invidious/config.cr +++ b/src/invidious/config.cr @@ -118,6 +118,10 @@ class Config # Connect to YouTube over 'ipv6', 'ipv4'. Will sometimes resolve fix issues with rate-limiting (see https://github.com/ytdl-org/youtube-dl/issues/21729) @[YAML::Field(converter: Preferences::FamilyConverter)] property force_resolve : Socket::Family = Socket::Family::UNSPEC + + # External signature solver server socket (either a path to a UNIX domain socket or ":") + property signature_server : String? = nil + # Port to listen for connections (overridden by command line argument) property port : Int32 = 3000 # Host to bind (overridden by command line argument) diff --git a/src/invidious/helpers/sig_helper.cr b/src/invidious/helpers/sig_helper.cr index 2239858b..13026321 100644 --- a/src/invidious/helpers/sig_helper.cr +++ b/src/invidious/helpers/sig_helper.cr @@ -72,8 +72,12 @@ module Invidious::SigHelper # High-level functions # ---------------------- - module Client - extend self + class Client + @mux : Multiplexor + + def initialize(uri_or_path) + @mux = Multiplexor.new(uri_or_path) + end # Forces the server to re-fetch the YouTube player, and extract the necessary # components from it (nsig function code, sig function code, signature timestamp). @@ -148,7 +152,7 @@ module Invidious::SigHelper end private def send_request(request : Request, &) - channel = Multiplexor::INSTANCE.send(request) + channel = @mux.send(request) slice = channel.receive return yield slice rescue ex @@ -172,10 +176,8 @@ module Invidious::SigHelper @conn : Connection - INSTANCE = new("") - - def initialize(url : String) - @conn = Connection.new(url) + def initialize(uri_or_path) + @conn = Connection.new(uri_or_path) listen end @@ -275,13 +277,14 @@ module Invidious::SigHelper {% end %} def initialize(host_or_path : String) - if host_or_path.empty? - host_or_path = "/tmp/inv_sig_helper.sock" - end - case host_or_path when .starts_with?('/') - @socket = UNIXSocket.new(host_or_path) + # Make sure that the file exists + if File.exists?(host_or_path) + @socket = UNIXSocket.new(host_or_path) + else + raise Exception.new("SigHelper: '#{host_or_path}' no such file") + end when .starts_with?("tcp://") uri = URI.parse(host_or_path) @socket = TCPSocket.new(uri.host.not_nil!, uri.port.not_nil!) diff --git a/src/invidious/helpers/signatures.cr b/src/invidious/helpers/signatures.cr index cf170668..a2abf327 100644 --- a/src/invidious/helpers/signatures.cr +++ b/src/invidious/helpers/signatures.cr @@ -1,10 +1,11 @@ require "http/params" require "./sig_helper" -struct Invidious::DecryptFunction +class Invidious::DecryptFunction @last_update : Time = Time.utc - 42.days - def initialize + def initialize(uri_or_path) + @client = SigHelper::Client.new(uri_or_path) self.check_update end @@ -16,19 +17,18 @@ struct Invidious::DecryptFunction # Get the time when the player was updated, in the event where # multiple invidious processes are run in parallel. - player_ts = Invidious::SigHelper::Client.get_player_timestamp - player_time = Time.unix(player_ts || 0) + player_time = Time.unix(@client.get_player_timestamp || 0) if (now - player_time) > 5.minutes LOGGER.debug("Signature: Player might be outdated, updating") - Invidious::SigHelper::Client.force_update + @client.force_update @last_update = Time.utc end end def decrypt_nsig(n : String) : String? self.check_update - return SigHelper::Client.decrypt_n_param(n) + return @client.decrypt_n_param(n) rescue ex LOGGER.debug(ex.message || "Signature: Unknown error") LOGGER.trace(ex.inspect_with_backtrace) @@ -37,7 +37,7 @@ struct Invidious::DecryptFunction def decrypt_signature(str : String) : String? self.check_update - return SigHelper::Client.decrypt_sig(str) + return @client.decrypt_sig(str) rescue ex LOGGER.debug(ex.message || "Signature: Unknown error") LOGGER.trace(ex.inspect_with_backtrace) @@ -46,7 +46,7 @@ struct Invidious::DecryptFunction def get_sts : UInt64? self.check_update - return SigHelper::Client.get_signature_timestamp + return @client.get_signature_timestamp rescue ex LOGGER.debug(ex.message || "Signature: Unknown error") LOGGER.trace(ex.inspect_with_backtrace) diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index ed172878..8e1e4aac 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -1,5 +1,3 @@ -private DECRYPT_FUNCTION = IV::DecryptFunction.new - enum VideoType Video Livestream @@ -108,14 +106,14 @@ struct Video LOGGER.debug("Videos: Decoding '#{cfr}'") - unsig = DECRYPT_FUNCTION.decrypt_signature(cfr["s"]) + unsig = DECRYPT_FUNCTION.try &.decrypt_signature(cfr["s"]) params[sp] = unsig if unsig else url = URI.parse(fmt["url"].as_s) params = url.query_params end - n = DECRYPT_FUNCTION.decrypt_nsig(params["n"]) + n = DECRYPT_FUNCTION.try &.decrypt_nsig(params["n"]) params["n"] = n if n params["host"] = url.host.not_nil! diff --git a/src/invidious/yt_backend/youtube_api.cr b/src/invidious/yt_backend/youtube_api.cr index f4ee35e5..09a5e7f4 100644 --- a/src/invidious/yt_backend/youtube_api.cr +++ b/src/invidious/yt_backend/youtube_api.cr @@ -2,8 +2,6 @@ # This file contains youtube API wrappers # -private STS_FETCHER = IV::DecryptFunction.new - module YoutubeAPI extend self @@ -462,7 +460,7 @@ module YoutubeAPI } of String => String | Int64 if {"WEB", "TVHTML5"}.any? { |s| client_config.name.starts_with? s } - if sts = STS_FETCHER.get_sts + if sts = DECRYPT_FUNCTION.try &.get_sts playback_ctx["signatureTimestamp"] = sts.to_i64 end end From cc36a8293359764c8df38605818242c60f41bbec Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Wed, 7 Aug 2024 23:23:24 +0200 Subject: [PATCH 131/155] SigHelper: Fix some logic errors raised during code review --- src/invidious/helpers/sig_helper.cr | 2 +- src/invidious/helpers/signatures.cr | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/invidious/helpers/sig_helper.cr b/src/invidious/helpers/sig_helper.cr index 13026321..9e72c1c7 100644 --- a/src/invidious/helpers/sig_helper.cr +++ b/src/invidious/helpers/sig_helper.cr @@ -144,7 +144,7 @@ module Invidious::SigHelper # Return when the player was last updated def get_player_timestamp : UInt64? - request = Request.new(Opcode::GET_SIGNATURE_TIMESTAMP, nil) + request = Request.new(Opcode::PLAYER_UPDATE_TIMESTAMP, nil) return self.send_request(request) do |bytes| IO::ByteFormat::NetworkEndian.decode(UInt64, bytes) diff --git a/src/invidious/helpers/signatures.cr b/src/invidious/helpers/signatures.cr index a2abf327..84a8a86d 100644 --- a/src/invidious/helpers/signatures.cr +++ b/src/invidious/helpers/signatures.cr @@ -15,11 +15,11 @@ class Invidious::DecryptFunction # If we have updated in the last 5 minutes, do nothing return if (now - @last_update) > 5.minutes - # Get the time when the player was updated, in the event where - # multiple invidious processes are run in parallel. - player_time = Time.unix(@client.get_player_timestamp || 0) + # Get the amount of time elapsed since when the player was updated, in the + # event where multiple invidious processes are run in parallel. + update_time_elapsed = (@client.get_player_timestamp || 301).seconds - if (now - player_time) > 5.minutes + if update_time_elapsed > 5.minutes LOGGER.debug("Signature: Player might be outdated, updating") @client.force_update @last_update = Time.utc From e6c39f9e3a29b1b701f18875f57114cb30c4b8dc Mon Sep 17 00:00:00 2001 From: Emilien Devos <4016501+unixfox@users.noreply.github.com> Date: Tue, 13 Aug 2024 14:37:35 +0200 Subject: [PATCH 132/155] add pot= parameter now required by youtube --- src/invidious/videos.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index cdfca02c..44ed53ee 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -110,7 +110,7 @@ struct Video fmt["url"] = JSON::Any.new("#{fmt["url"]}#{DECRYPT_FUNCTION.decrypt_signature(fmt)}") end - fmt["url"] = JSON::Any.new("#{fmt["url"]}&host=#{URI.parse(fmt["url"].as_s).host}") + fmt["url"] = JSON::Any.new("#{fmt["url"]}&host=#{URI.parse(fmt["url"].as_s).host}&pot=#{CONFIG.po_token}") fmt["url"] = JSON::Any.new("#{fmt["url"]}®ion=#{self.info["region"]}") if self.info["region"]? end @@ -130,7 +130,7 @@ struct Video fmt["url"] = JSON::Any.new("#{fmt["url"]}#{DECRYPT_FUNCTION.decrypt_signature(fmt)}") end - fmt["url"] = JSON::Any.new("#{fmt["url"]}&host=#{URI.parse(fmt["url"].as_s).host}") + fmt["url"] = JSON::Any.new("#{fmt["url"]}&host=#{URI.parse(fmt["url"].as_s).host}&pot=#{CONFIG.po_token}") fmt["url"] = JSON::Any.new("#{fmt["url"]}®ion=#{self.info["region"]}") if self.info["region"]? end From 4b8bfe1201ab84617f0335054dea7d2334fd7418 Mon Sep 17 00:00:00 2001 From: Emilien Devos <4016501+unixfox@users.noreply.github.com> Date: Tue, 13 Aug 2024 15:02:02 +0200 Subject: [PATCH 133/155] use docker compose instead of docker-compose for CI --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 925a8fc7..de538915 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -90,10 +90,10 @@ jobs: - uses: actions/checkout@v4 - name: Build Docker - run: docker-compose build --build-arg release=0 + run: docker compose build --build-arg release=0 - name: Run Docker - run: docker-compose up -d + run: docker compose up -d - name: Test Docker run: while curl -Isf http://localhost:3000; do sleep 1; done From c9fb19431d14345d2c41209833ea63a85cefa1bd Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 13 Aug 2024 19:51:36 +0200 Subject: [PATCH 134/155] Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Update Bulgarian translation Update German translation Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Co-authored-by: Hosted Weblate Co-authored-by: Jose Delvani Co-authored-by: Least Significant Bite Co-authored-by: NEXI Co-authored-by: Radoslav Lelchev Co-authored-by: Random Co-authored-by: Unacceptium Co-authored-by: hiatsu0 --- 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 1637b5d8..0887e697 100644 --- a/locales/pt-BR.json +++ b/locales/pt-BR.json @@ -41,7 +41,7 @@ "Time (h:mm:ss):": "Hora (h:mm:ss):", "Text CAPTCHA": "Mudar para um desafio de texto", "Image CAPTCHA": "Mudar para um desafio visual", - "Sign In": "Entrar", + "Sign In": "Fazer login", "Register": "Criar conta", "E-mail": "E-mail", "Preferences": "Preferências", From f842033eb550e7bf2cf80ee4bdedf2f3e1aacee2 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 13 Aug 2024 19:51:36 +0200 Subject: [PATCH 135/155] Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Update Bulgarian translation Update German translation Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Co-authored-by: Hosted Weblate Co-authored-by: Jose Delvani Co-authored-by: Least Significant Bite Co-authored-by: NEXI Co-authored-by: Radoslav Lelchev Co-authored-by: Random Co-authored-by: Unacceptium Co-authored-by: hiatsu0 --- locales/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/de.json b/locales/de.json index 46327f57..d20f7fab 100644 --- a/locales/de.json +++ b/locales/de.json @@ -21,7 +21,7 @@ "Import and Export Data": "Daten importieren und exportieren", "Import": "Importieren", "Import Invidious data": "Invidious-JSON-Daten importieren", - "Import YouTube subscriptions": "YouTube-/OPML-Abonnements importieren", + "Import YouTube subscriptions": "YouTube-CSV/OPML-Abonnements importieren", "Import FreeTube subscriptions (.db)": "FreeTube Abonnements importieren (.db)", "Import NewPipe subscriptions (.json)": "NewPipe Abonnements importieren (.json)", "Import NewPipe data (.zip)": "NewPipe Daten importieren (.zip)", From 7cf7cce0b2f6ec5fc2b38f6e0685e4095adf701d Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 13 Aug 2024 19:51:36 +0200 Subject: [PATCH 136/155] Update Greek translation Update Greek translation Co-authored-by: Hosted Weblate Co-authored-by: Open Contribution Co-authored-by: mpt.c --- locales/el.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/locales/el.json b/locales/el.json index 1d827eba..902c8b97 100644 --- a/locales/el.json +++ b/locales/el.json @@ -486,5 +486,8 @@ "Switch Invidious Instance": "Αλλαγή Instance Invidious", "Standard YouTube license": "Τυπική άδεια YouTube", "search_filters_duration_option_medium": "Μεσαία (4 - 20 λεπτά)", - "search_filters_date_label": "Ημερομηνία αναφόρτωσης" + "search_filters_date_label": "Ημερομηνία αναφόρτωσης", + "Search for videos": "Αναζήτηση βίντεο", + "The Popular feed has been disabled by the administrator.": "Η δημοφιλής ροή έχει απενεργοποιηθεί από τον διαχειριστή.", + "Answer": "Απάντηση" } From e99b5918553eec8571c894b72e9d106b7665f840 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 13 Aug 2024 19:51:36 +0200 Subject: [PATCH 137/155] Update Russian translation Co-authored-by: Hosted Weblate Co-authored-by: Stepan --- locales/ru.json | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/locales/ru.json b/locales/ru.json index 61bf9e92..efdaa640 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -21,7 +21,7 @@ "Import and Export Data": "Импорт и экспорт данных", "Import": "Импорт", "Import Invidious data": "Импортировать JSON с данными Invidious", - "Import YouTube subscriptions": "Импортировать подписки из YouTube/OPML", + "Import YouTube subscriptions": "Импортировать подписки из CSV или OPML", "Import FreeTube subscriptions (.db)": "Импортировать подписки из FreeTube (.db)", "Import NewPipe subscriptions (.json)": "Импортировать подписки из NewPipe (.json)", "Import NewPipe data (.zip)": "Импортировать данные из NewPipe (.zip)", @@ -504,5 +504,11 @@ "generic_channels_count_0": "{{count}} канал", "generic_channels_count_1": "{{count}} канала", "generic_channels_count_2": "{{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.": "Популярная лента была отключена администратором.", + "toggle_theme": "Переключатель тем" } From 84aded85c5a31c20a0faf32d3a153ecff1575863 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 13 Aug 2024 19:51:36 +0200 Subject: [PATCH 138/155] Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Update Bulgarian translation Update German translation Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Co-authored-by: Hosted Weblate Co-authored-by: Jose Delvani Co-authored-by: Least Significant Bite Co-authored-by: NEXI Co-authored-by: Radoslav Lelchev Co-authored-by: Random Co-authored-by: Unacceptium Co-authored-by: hiatsu0 --- locales/bg.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/locales/bg.json b/locales/bg.json index bcce6a7a..baa683c9 100644 --- a/locales/bg.json +++ b/locales/bg.json @@ -487,5 +487,11 @@ "generic_views_count": "{{count}} гледане", "generic_views_count_plural": "{{count}} гледания", "Next page": "Следваща страница", - "Import YouTube watch history (.json)": "Импортиране на историята на гледане от YouTube (.json)" + "Import YouTube watch history (.json)": "Импортиране на историята на гледане от YouTube (.json)", + "toggle_theme": "Смени темата", + "Add to playlist": "Добави към плейлист", + "Add to playlist: ": "Добави към плейлист: ", + "Answer": "Отговор", + "Search for videos": "Търсене на видеа", + "The Popular feed has been disabled by the administrator.": "Популярната страница е деактивирана от администратора." } From 456b00a699e2c672e3c231bdbbe73aed8202ec15 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 13 Aug 2024 19:51:36 +0200 Subject: [PATCH 139/155] Update Ukrainian translation Co-authored-by: Hosted Weblate Co-authored-by: Ihor Hordiichuk --- locales/uk.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/uk.json b/locales/uk.json index 223772d9..5d008fa3 100644 --- a/locales/uk.json +++ b/locales/uk.json @@ -21,7 +21,7 @@ "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)": "Імпортувати підписки з FreeTube (.db)", "Import NewPipe subscriptions (.json)": "Імпортувати підписки з NewPipe (.json)", "Import NewPipe data (.zip)": "Імпортувати дані з NewPipe (.zip)", From 5cb1688c784a08a259e8f159287c3bb497a62295 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 13 Aug 2024 19:51:36 +0200 Subject: [PATCH 140/155] Update Catalan translation Co-authored-by: Daniel Co-authored-by: Hosted Weblate --- locales/ca.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/locales/ca.json b/locales/ca.json index 4ae55804..bbcadf89 100644 --- a/locales/ca.json +++ b/locales/ca.json @@ -487,5 +487,7 @@ "generic_button_edit": "Edita", "generic_button_rss": "RSS", "generic_button_delete": "Suprimeix", - "Import YouTube watch history (.json)": "Importa l'historial de visualitzacions de YouTube (.json)" + "Import YouTube watch history (.json)": "Importa l'historial de visualitzacions de YouTube (.json)", + "Answer": "Resposta", + "toggle_theme": "Commuta el tema" } From 2d485b18a44cf91c7a8cc4adc55db5179669ceea Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 13 Aug 2024 19:51:36 +0200 Subject: [PATCH 141/155] Update Welsh translation Add Welsh translation Co-authored-by: Hosted Weblate Co-authored-by: newidyn --- locales/cy.json | 385 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 385 insertions(+) create mode 100644 locales/cy.json diff --git a/locales/cy.json b/locales/cy.json new file mode 100644 index 00000000..566e73e1 --- /dev/null +++ b/locales/cy.json @@ -0,0 +1,385 @@ +{ + "Time (h:mm:ss):": "Amser (h:mm:ss):", + "Password": "Cyfrinair", + "preferences_quality_dash_option_auto": "Awtomatig", + "preferences_quality_dash_option_best": "Gorau", + "preferences_quality_dash_option_worst": "Gwaethaf", + "preferences_quality_dash_option_360p": "360p", + "published": "dyddiad cyhoeddi", + "preferences_quality_dash_option_4320p": "4320p", + "preferences_quality_dash_option_480p": "480p", + "preferences_quality_dash_option_240p": "240p", + "preferences_quality_dash_option_144p": "144p", + "preferences_comments_label": "Ffynhonnell sylwadau: ", + "preferences_captions_label": "Isdeitlau rhagosodedig: ", + "youtube": "YouTube", + "reddit": "Reddit", + "Fallback captions: ": "Isdeitlau amgen: ", + "preferences_related_videos_label": "Dangos fideos perthnasol: ", + "dark": "tywyll", + "preferences_dark_mode_label": "Thema: ", + "light": "golau", + "preferences_sort_label": "Trefnu fideo yn ôl: ", + "Import/export data": "Mewnforio/allforio data", + "Delete account": "Dileu eich cyfrif", + "preferences_category_admin": "Hoffterau gweinyddu", + "playlist_button_add_items": "Ychwanegu fideos", + "Delete playlist": "Dileu'r rhestr chwarae", + "Create playlist": "Creu rhestr chwarae", + "Show less": "Dangos llai", + "Show more": "Dangos rhagor", + "Watch on YouTube": "Gwylio ar YouTube", + "search_message_no_results": "Dim canlyniadau.", + "search_message_change_filters_or_query": "Ceisiwch ehangu eich chwiliad ac/neu newid yr hidlyddion.", + "License: ": "Trwydded: ", + "Standard YouTube license": "Trwydded safonol YouTube", + "Family friendly? ": "Addas i bawb? ", + "Wilson score: ": "Sgôr Wilson: ", + "Show replies": "Dangos ymatebion", + "Music in this video": "Cerddoriaeth yn y fideo hwn", + "Artist: ": "Artist: ", + "Erroneous CAPTCHA": "CAPTCHA anghywir", + "This channel does not exist.": "Dyw'r sianel hon ddim yn bodoli.", + "Not a playlist.": "Ddim yn rhestr chwarae.", + "Could not fetch comments": "Wedi methu llwytho sylwadau", + "Playlist does not exist.": "Dyw'r rhestr chwarae ddim yn bodoli.", + "Erroneous challenge": "Her annilys", + "channel_tab_podcasts_label": "Podlediadau", + "channel_tab_playlists_label": "Rhestrau chwarae", + "channel_tab_streams_label": "Fideos byw", + "crash_page_read_the_faq": "darllen y cwestiynau cyffredin", + "crash_page_switch_instance": "ceisio defnyddio gweinydd arall", + "crash_page_refresh": "ceisio ail-lwytho'r dudalen", + "search_filters_features_option_four_k": "4K", + "search_filters_features_label": "Nodweddion", + "search_filters_duration_option_medium": "Canolig (4 - 20 munud)", + "search_filters_features_option_live": "Yn fyw", + "search_filters_duration_option_long": "Hir (> 20 munud)", + "search_filters_date_option_year": "Eleni", + "search_filters_type_label": "Math", + "search_filters_date_option_month": "Y mis hwn", + "generic_views_count_0": "{{count}} o wyliadau", + "generic_views_count_1": "{{count}} gwyliad", + "generic_views_count_2": "{{count}} wyliad", + "generic_views_count_3": "{{count}} o wyliadau", + "generic_views_count_4": "{{count}} o wyliadau", + "generic_views_count_5": "{{count}} o wyliadau", + "Answer": "Ateb", + "Add to playlist: ": "Ychwanegu at y rhestr chwarae: ", + "Add to playlist": "Ychwanegu at y rhestr chwarae", + "generic_button_cancel": "Diddymu", + "generic_button_rss": "RSS", + "LIVE": "YN FYW", + "Import YouTube watch history (.json)": "Mewnforio hanes gwylio YouTube (.json)", + "generic_videos_count_0": "{{count}} fideo", + "generic_videos_count_1": "{{count}} fideo", + "generic_videos_count_2": "{{count}} fideo", + "generic_videos_count_3": "{{count}} fideo", + "generic_videos_count_4": "{{count}} fideo", + "generic_videos_count_5": "{{count}} fideo", + "generic_subscribers_count_0": "{{count}} tanysgrifiwr", + "generic_subscribers_count_1": "{{count}} tanysgrifiwr", + "generic_subscribers_count_2": "{{count}} danysgrifiwr", + "generic_subscribers_count_3": "{{count}} thanysgrifiwr", + "generic_subscribers_count_4": "{{count}} o danysgrifwyr", + "generic_subscribers_count_5": "{{count}} o danysgrifwyr", + "Authorize token?": "Awdurdodi'r tocyn?", + "Authorize token for `x`?": "Awdurdodi'r tocyn ar gyfer `x`?", + "English": "Saesneg", + "English (United Kingdom)": "Saesneg (Y Deyrnas Unedig)", + "English (United States)": "Saesneg (Yr Unol Daleithiau)", + "Afrikaans": "Affricaneg", + "English (auto-generated)": "Saesneg (awtomatig)", + "Amharic": "Amhareg", + "Albanian": "Albaneg", + "Arabic": "Arabeg", + "crash_page_report_issue": "Os nad yw'r awgrymiadau uchod wedi helpu, codwch 'issue' newydd ar Github (yn Saesneg, gorau oll) a chynnwys y testun canlynol yn eich neges (peidiwch â chyfieithu'r testun hwn):", + "Search for videos": "Chwilio am fideos", + "The Popular feed has been disabled by the administrator.": "Mae'r ffrwd fideos poblogaidd wedi ei hanalluogi gan y gweinyddwr.", + "generic_channels_count_0": "{{count}} sianel", + "generic_channels_count_1": "{{count}} sianel", + "generic_channels_count_2": "{{count}} sianel", + "generic_channels_count_3": "{{count}} sianel", + "generic_channels_count_4": "{{count}} sianel", + "generic_channels_count_5": "{{count}} sianel", + "generic_button_delete": "Dileu", + "generic_button_edit": "Golygu", + "generic_button_save": "Cadw", + "Shared `x` ago": "Rhannwyd `x` yn ôl", + "Unsubscribe": "Dad-danysgrifio", + "Subscribe": "Tanysgrifio", + "View channel on YouTube": "Gweld y sianel ar YouTube", + "View playlist on YouTube": "Gweld y rhestr chwarae ar YouTube", + "newest": "diweddaraf", + "oldest": "hynaf", + "popular": "poblogaidd", + "Next page": "Tudalen nesaf", + "Previous page": "Tudalen flaenorol", + "Clear watch history?": "Clirio'ch hanes gwylio?", + "New password": "Cyfrinair newydd", + "Import and Export Data": "Mewnforio ac allforio data", + "Import": "Mewnforio", + "Import Invidious data": "Mewnforio data JSON Invidious", + "Import YouTube subscriptions": "Mewnforio tanysgrifiadau YouTube ar fformat CSV neu OPML", + "Import YouTube playlist (.csv)": "Mewnforio rhestr chwarae YouTube (.csv)", + "Export": "Allforio", + "Export data as JSON": "Allforio data Invidious ar fformat JSON", + "Delete account?": "Ydych chi'n siŵr yr hoffech chi ddileu eich cyfrif?", + "History": "Hanes", + "JavaScript license information": "Gwybodaeth am y drwydded JavaScript", + "generic_subscriptions_count_0": "{{count}} tanysgrifiad", + "generic_subscriptions_count_1": "{{count}} tanysgrifiad", + "generic_subscriptions_count_2": "{{count}} danysgrifiad", + "generic_subscriptions_count_3": "{{count}} thanysgrifiad", + "generic_subscriptions_count_4": "{{count}} o danysgrifiadau", + "generic_subscriptions_count_5": "{{count}} o danysgrifiadau", + "Yes": "Iawn", + "No": "Na", + "Import FreeTube subscriptions (.db)": "Mewnforio tanysgrifiadau FreeTube (.db)", + "Import NewPipe subscriptions (.json)": "Mewnforio tanysgrifiadau NewPipe (.json)", + "Import NewPipe data (.zip)": "Mewnforio data NewPipe (.zip)", + "An alternative front-end to YouTube": "Pen blaen amgen i YouTube", + "source": "ffynhonnell", + "Log in": "Mewngofnodi", + "Log in/register": "Mewngofnodi/Cofrestru", + "User ID": "Enw defnyddiwr", + "preferences_quality_option_dash": "DASH (ansawdd addasol)", + "Sign In": "Mewngofnodi", + "Register": "Cofrestru", + "E-mail": "Ebost", + "Preferences": "Hoffterau", + "preferences_category_player": "Hoffterau'r chwaraeydd", + "preferences_autoplay_label": "Chwarae'n awtomatig: ", + "preferences_local_label": "Llwytho fideos drwy ddirprwy weinydd: ", + "preferences_watch_history_label": "Galluogi hanes gwylio: ", + "preferences_speed_label": "Cyflymder rhagosodedig: ", + "preferences_quality_label": "Ansawdd fideos: ", + "preferences_quality_option_hd720": "HD720", + "preferences_quality_option_medium": "Canolig", + "preferences_quality_option_small": "Bach", + "preferences_quality_dash_option_2160p": "2160p", + "preferences_quality_dash_option_1440p": "1440p", + "preferences_quality_dash_option_1080p": "1080p", + "preferences_quality_dash_option_720p": "720p", + "invidious": "Invidious", + "Text CAPTCHA": "CAPTCHA testun", + "Image CAPTCHA": "CAPTCHA delwedd", + "preferences_continue_label": "Chwarae'r fideo nesaf fel rhagosodiad: ", + "preferences_continue_autoplay_label": "Chwarae'r fideo nesaf yn awtomatig: ", + "preferences_listen_label": "Sain yn unig: ", + "preferences_quality_dash_label": "Ansawdd fideos DASH a ffefrir: ", + "preferences_volume_label": "Uchder sain y chwaraeydd: ", + "preferences_category_visual": "Hoffterau'r wefan", + "preferences_region_label": "Gwlad y cynnwys: ", + "preferences_player_style_label": "Arddull y chwaraeydd: ", + "Dark mode: ": "Modd tywyll: ", + "preferences_thin_mode_label": "Modd tenau: ", + "preferences_category_misc": "Hoffterau amrywiol", + "preferences_category_subscription": "Hoffterau tanysgrifio", + "preferences_max_results_label": "Nifer o fideos a ddangosir yn eich ffrwd: ", + "alphabetically": "yr wyddor", + "alphabetically - reverse": "yr wyddor - am yn ôl", + "published - reverse": "dyddiad cyhoeddi - am yn ôl", + "channel name": "enw'r sianel", + "channel name - reverse": "enw'r sianel - am yn ôl", + "Only show latest video from channel: ": "Dangos fideo diweddaraf y sianeli rydych chi'n tanysgrifio iddynt: ", + "Only show latest unwatched video from channel: ": "Dangos fideo heb ei wylio diweddaraf y sianeli rydych chi'n tanysgrifio iddynt: ", + "Enable web notifications": "Galluogi hysbysiadau gwe", + "`x` uploaded a video": "uwchlwythodd `x` fideo", + "`x` is live": "mae `x` yn darlledu'n fyw", + "preferences_category_data": "Hoffterau data", + "Clear watch history": "Clirio'ch hanes gwylio", + "Change password": "Newid eich cyfrinair", + "Manage subscriptions": "Rheoli tanysgrifiadau", + "Manage tokens": "Rheoli tocynnau", + "Watch history": "Hanes gwylio", + "preferences_default_home_label": "Hafan ragosodedig: ", + "preferences_show_nick_label": "Dangos eich enw defnyddiwr ar frig y dudalen: ", + "preferences_annotations_label": "Dangos nodiadau fel rhagosodiad: ", + "preferences_unseen_only_label": "Dangos fideos heb eu gwylio yn unig: ", + "preferences_notifications_only_label": "Dangos hysbysiadau yn unig (os oes unrhyw rai): ", + "Token manager": "Rheolydd tocynnau", + "Token": "Tocyn", + "unsubscribe": "dad-danysgrifio", + "Subscriptions": "Tanysgrifiadau", + "Import/export": "Mewngofnodi/allgofnodi", + "search": "chwilio", + "Log out": "Allgofnodi", + "View privacy policy.": "Polisi preifatrwydd", + "Trending": "Pynciau llosg", + "Public": "Cyhoeddus", + "Private": "Preifat", + "Updated `x` ago": "Diweddarwyd `x` yn ôl", + "Delete playlist `x`?": "Ydych chi'n siŵr yr hoffech chi ddileu'r rhestr chwarae `x`?", + "Title": "Teitl", + "Playlist privacy": "Preifatrwydd y rhestr chwarae", + "search_message_use_another_instance": " Gallwch hefyd chwilio ar weinydd arall.", + "Popular enabled: ": "Tudalen fideos poblogaidd wedi'i galluogi: ", + "CAPTCHA enabled: ": "CAPTCHA wedi'i alluogi: ", + "Registration enabled: ": "Cofrestru wedi'i alluogi: ", + "Save preferences": "Cadw'r hoffterau", + "Subscription manager": "Rheolydd tanysgrifio", + "revoke": "tynnu", + "subscriptions_unseen_notifs_count_0": "{{count}} hysbysiad heb ei weld", + "subscriptions_unseen_notifs_count_1": "{{count}} hysbysiad heb ei weld", + "subscriptions_unseen_notifs_count_2": "{{count}} hysbysiad heb eu gweld", + "subscriptions_unseen_notifs_count_3": "{{count}} hysbysiad heb eu gweld", + "subscriptions_unseen_notifs_count_4": "{{count}} hysbysiad heb eu gweld", + "subscriptions_unseen_notifs_count_5": "{{count}} hysbysiad heb eu gweld", + "Released under the AGPLv3 on Github.": "Cyhoeddwyd dan drwydded AGPLv3 ar GitHub", + "Unlisted": "Heb ei restru", + "Switch Invidious Instance": "Newid gweinydd Invidious", + "Report statistics: ": "Galluogi ystadegau'r gweinydd: ", + "View all playlists": "Gweld pob rhestr chwarae", + "Editing playlist `x`": "Yn golygu'r rhestr chwarae `x`", + "Whitelisted regions: ": "Rhanbarthau a ganiateir: ", + "Blacklisted regions: ": "Rhanbarthau a rwystrir: ", + "Song: ": "Cân: ", + "Album: ": "Albwm: ", + "Shared `x`": "Rhannwyd `x`", + "View YouTube comments": "Dangos sylwadau YouTube", + "View more comments on Reddit": "Dangos rhagor o sylwadau ar Reddit", + "View Reddit comments": "Dangos sylwadau Reddit", + "Hide replies": "Cuddio ymatebion", + "Incorrect password": "Cyfrinair anghywir", + "Wrong answer": "Ateb anghywir", + "CAPTCHA is a required field": "Rhaid rhoi'r CAPTCHA", + "User ID is a required field": "Rhaid rhoi enw defnyddiwr", + "Password is a required field": "Rhaid rhoi cyfrinair", + "Wrong username or password": "Enw defnyddiwr neu gyfrinair anghywir", + "Password cannot be empty": "All y cyfrinair ddim bod yn wag", + "Password cannot be longer than 55 characters": "All y cyfrinair ddim bod yn hirach na 55 nod", + "Please log in": "Mewngofnodwch", + "channel:`x`": "sianel: `x`", + "Deleted or invalid channel": "Sianel wedi'i dileu neu'n annilys", + "Could not get channel info.": "Wedi methu llwytho gwybodaeth y sianel.", + "`x` ago": "`x` yn ôl", + "Load more": "Llwytho rhagor", + "Empty playlist": "Rhestr chwarae wag", + "Hide annotations": "Cuddio nodiadau", + "Show annotations": "Dangos nodiadau", + "Premieres in `x`": "Yn dechrau mewn `x`", + "Premieres `x`": "Yn dechrau `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.": "Helo! Mae'n ymddangos eich bod wedi diffodd JavaScript. Cliciwch yma i weld sylwadau, ond cofiwch y gall gymryd mwy o amser i'w llwytho.", + "View `x` comments": { + "([^.,0-9]|^)1([^.,0-9]|$)": "Gweld `x` sylw", + "": "Gweld `x` sylw" + }, + "Could not create mix.": "Wedi methu creu'r cymysgiad hwn.", + "Erroneous token": "Tocyn annilys", + "No such user": "Dyw'r defnyddiwr hwn ddim yn bodoli", + "Token is expired, please try again": "Mae'r tocyn hwn wedi dod i ben, ceisiwch eto", + "Bangla": "Bangleg", + "Basque": "Basgeg", + "Bulgarian": "Bwlgareg", + "Catalan": "Catalaneg", + "Chinese": "Tsieineeg", + "Chinese (China)": "Tsieineeg (Tsieina)", + "Chinese (Hong Kong)": "Tsieineeg (Hong Kong)", + "Chinese (Taiwan)": "Tsieineeg (Taiwan)", + "Danish": "Daneg", + "Dutch": "Iseldireg", + "Esperanto": "Esperanteg", + "Finnish": "Ffinneg", + "French": "Ffrangeg", + "German": "Almaeneg", + "Greek": "Groeg", + "Could not pull trending pages.": "Wedi methu llwytho tudalennau pynciau llosg.", + "Hidden field \"challenge\" is a required field": "Mae'r maes cudd \"her\" yn ofynnol", + "Hidden field \"token\" is a required field": "Mae'r maes cudd \"tocyn\" yn ofynnol", + "Hebrew": "Hebraeg", + "Hungarian": "Hwngareg", + "Irish": "Gwyddeleg", + "Italian": "Eidaleg", + "Welsh": "Cymraeg", + "generic_count_hours_0": "{{count}} awr", + "generic_count_hours_1": "{{count}} awr", + "generic_count_hours_2": "{{count}} awr", + "generic_count_hours_3": "{{count}} awr", + "generic_count_hours_4": "{{count}} awr", + "generic_count_hours_5": "{{count}} awr", + "generic_count_minutes_0": "{{count}} munud", + "generic_count_minutes_1": "{{count}} munud", + "generic_count_minutes_2": "{{count}} funud", + "generic_count_minutes_3": "{{count}} munud", + "generic_count_minutes_4": "{{count}} o funudau", + "generic_count_minutes_5": "{{count}} o funudau", + "generic_count_weeks_0": "{{count}} wythnos", + "generic_count_weeks_1": "{{count}} wythnos", + "generic_count_weeks_2": "{{count}} wythnos", + "generic_count_weeks_3": "{{count}} wythnos", + "generic_count_weeks_4": "{{count}} wythnos", + "generic_count_weeks_5": "{{count}} wythnos", + "generic_count_seconds_0": "{{count}} eiliad", + "generic_count_seconds_1": "{{count}} eiliad", + "generic_count_seconds_2": "{{count}} eiliad", + "generic_count_seconds_3": "{{count}} eiliad", + "generic_count_seconds_4": "{{count}} o eiliadau", + "generic_count_seconds_5": "{{count}} o eiliadau", + "Fallback comments: ": "Sylwadau amgen: ", + "Popular": "Poblogaidd", + "preferences_locale_label": "Iaith: ", + "About": "Ynghylch", + "Search": "Chwilio", + "search_filters_features_option_c_commons": "Comin Creu", + "search_filters_features_option_subtitles": "Isdeitlau (CC)", + "search_filters_features_option_hd": "HD", + "permalink": "dolen barhaol", + "search_filters_duration_option_short": "Byr (< 4 munud)", + "search_filters_duration_option_none": "Unrhyw hyd", + "search_filters_duration_label": "Hyd", + "search_filters_type_option_show": "Rhaglen", + "search_filters_type_option_movie": "Ffilm", + "search_filters_type_option_playlist": "Rhestr chwarae", + "search_filters_type_option_channel": "Sianel", + "search_filters_type_option_video": "Fideo", + "search_filters_type_option_all": "Unrhyw fath", + "search_filters_date_option_week": "Yr wythnos hon", + "search_filters_date_option_today": "Heddiw", + "search_filters_date_option_hour": "Yr awr ddiwethaf", + "search_filters_date_option_none": "Unrhyw ddyddiad", + "search_filters_date_label": "Dyddiad uwchlwytho", + "search_filters_title": "Hidlyddion", + "Playlists": "Rhestrau chwarae", + "Video mode": "Modd fideo", + "Audio mode": "Modd sain", + "Channel Sponsor": "Noddwr y sianel", + "(edited)": "(golygwyd)", + "Download": "Islwytho", + "Movies": "Ffilmiau", + "News": "Newyddion", + "Gaming": "Gemau", + "Music": "Cerddoriaeth", + "Download is disabled": "Mae islwytho wedi'i analluogi", + "Download as: ": "Islwytho fel: ", + "View as playlist": "Gweld fel rhestr chwarae", + "Default": "Rhagosodiad", + "YouTube comment permalink": "Dolen barhaol i'r sylw ar YouTube", + "crash_page_before_reporting": "Cyn adrodd nam, sicrhewch eich bod wedi:", + "crash_page_search_issue": "chwilio am y nam ar GitHub", + "videoinfo_watch_on_youTube": "Gwylio ar YouTube", + "videoinfo_started_streaming_x_ago": "Yn ffrydio'n fyw ers `x` o funudau", + "videoinfo_invidious_embed_link": "Dolen mewnblannu", + "footer_documentation": "Dogfennaeth", + "footer_donate_page": "Rhoddi", + "Current version: ": "Fersiwn gyfredol: ", + "search_filters_apply_button": "Rhoi'r hidlyddion ar waith", + "search_filters_sort_option_date": "Dyddiad uwchlwytho", + "search_filters_sort_option_relevance": "Perthnasedd", + "search_filters_sort_label": "Trefnu yn ôl", + "search_filters_features_option_location": "Lleoliad", + "search_filters_features_option_hdr": "HDR", + "search_filters_features_option_three_d": "3D", + "search_filters_features_option_vr180": "VR180", + "search_filters_features_option_three_sixty": "360°", + "videoinfo_youTube_embed_link": "Mewnblannu", + "download_subtitles": "Isdeitlau - `x` (.vtt)", + "user_created_playlists": "`x` rhestr chwarae wedi'u creu", + "user_saved_playlists": "`x` rhestr chwarae wedi'u cadw", + "Video unavailable": "Fideo ddim ar gael", + "crash_page_you_found_a_bug": "Mae'n debyg eich bod wedi dod o hyd i nam yn Invidious!", + "channel_tab_channels_label": "Sianeli", + "channel_tab_community_label": "Cymuned", + "channel_tab_shorts_label": "Fideos byrion", + "channel_tab_videos_label": "Fideos" +} From 53a60bf7bd04aa9200d48ed0b141cb0443bc3c7f Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 13 Aug 2024 19:51:36 +0200 Subject: [PATCH 142/155] Update Portuguese translation Co-authored-by: Hosted Weblate Co-authored-by: Sergio Marques --- locales/pt.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/pt.json b/locales/pt.json index 463dbf3a..304e9cda 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -253,7 +253,7 @@ "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 via YouTube/OPML", + "Import YouTube subscriptions": "Importar via YouTube csv ou subscrição OPML", "Import Invidious data": "Importar dados JSON do Invidious", "Import": "Importar", "No": "Não", From 32ea9cfe167a8cf11868a05efbf82603317b57ed Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 13 Aug 2024 19:51:36 +0200 Subject: [PATCH 143/155] Update Icelandic translation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Hosted Weblate Co-authored-by: Sveinn í Felli --- locales/is.json | 293 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 239 insertions(+), 54 deletions(-) diff --git a/locales/is.json b/locales/is.json index ea4c4693..49f3711e 100644 --- a/locales/is.json +++ b/locales/is.json @@ -1,39 +1,39 @@ { "LIVE": "BEINT", - "Shared `x` ago": "Deilt `x` síðan", + "Shared `x` ago": "Deilt fyrir `x` síðan", "Unsubscribe": "Afskrá", "Subscribe": "Áskrifa", "View channel on YouTube": "Skoða rás á YouTube", - "View playlist on YouTube": "Skoða spilunarlisti á YouTube", + "View playlist on YouTube": "Skoða spilunarlista á YouTube", "newest": "nýjasta", "oldest": "elsta", "popular": "vinsælt", "last": "síðast", "Next page": "Næsta síða", "Previous page": "Fyrri síða", - "Clear watch history?": "Hreinsa áhorfssögu?", + "Clear watch history?": "Hreinsa áhorfsferil?", "New password": "Nýtt lykilorð", "New passwords must match": "Nýtt lykilorð verður að passa", - "Authorize token?": "Leyfa tákn?", - "Authorize token for `x`?": "Leyfa tákn fyrir `x`?", + "Authorize token?": "Leyfa teikn?", + "Authorize token for `x`?": "Leyfa teikn fyrir `x`?", "Yes": "Já", "No": "Nei", - "Import and Export Data": "Innflutningur og Útflutningur Gagna", + "Import and Export Data": "Inn- og útflutningur gagna", "Import": "Flytja inn", - "Import Invidious data": "Flytja inn Invidious gögn", - "Import YouTube subscriptions": "Flytja inn YouTube áskriftir", + "Import Invidious data": "Flytja inn Invidious JSON-gögn", + "Import YouTube subscriptions": "Flytja inn YouTube CSV eða OPML-áskriftir", "Import FreeTube subscriptions (.db)": "Flytja inn FreeTube áskriftir (.db)", "Import NewPipe subscriptions (.json)": "Flytja inn NewPipe áskriftir (.json)", "Import NewPipe data (.zip)": "Flytja inn NewPipe gögn (.zip)", "Export": "Flytja út", "Export subscriptions as OPML": "Flytja út áskriftir sem OPML", "Export subscriptions as OPML (for NewPipe & FreeTube)": "Flytja út áskriftir sem OPML (fyrir NewPipe & FreeTube)", - "Export data as JSON": "Flytja út gögn sem JSON", + "Export data as JSON": "Flytja út Invidious-gögn sem JSON", "Delete account?": "Eyða reikningi?", - "History": "Saga", - "An alternative front-end to YouTube": "Önnur framhlið fyrir YouTube", - "JavaScript license information": "JavaScript leyfi upplýsingar", - "source": "uppspretta", + "History": "Ferill", + "An alternative front-end to YouTube": "Annað viðmót fyrir YouTube", + "JavaScript license information": "Upplýsingar um notkunarleyfi JavaScript", + "source": "uppruni", "Log in": "Skrá inn", "Log in/register": "Innskráning/nýskráning", "User ID": "Notandakenni", @@ -47,33 +47,33 @@ "Preferences": "Kjörstillingar", "preferences_category_player": "Kjörstillingar spilara", "preferences_video_loop_label": "Alltaf lykkja: ", - "preferences_autoplay_label": "Spila sjálfkrafa: ", + "preferences_autoplay_label": "Sjálfvirk spilun: ", "preferences_continue_label": "Spila næst sjálfgefið: ", - "preferences_continue_autoplay_label": "Spila næst sjálfkrafa: ", + "preferences_continue_autoplay_label": "Spila næsta myndskeið sjálfkrafa: ", "preferences_listen_label": "Hlusta sjálfgefið: ", - "preferences_local_label": "Proxy myndbönd? ", + "preferences_local_label": "Milliþjónn fyrir myndskeið: ", "preferences_speed_label": "Sjálfgefinn hraði: ", - "preferences_quality_label": "Æskilegt myndbands gæði: ", + "preferences_quality_label": "Æskileg gæði myndmerkis: ", "preferences_volume_label": "Spilara hljóðstyrkur: ", "preferences_comments_label": "Sjálfgefin ummæli: ", "youtube": "YouTube", - "reddit": "reddit", + "reddit": "Reddit", "preferences_captions_label": "Sjálfgefin texti: ", "Fallback captions: ": "Varatextar: ", - "preferences_related_videos_label": "Sýna tengd myndbönd? ", + "preferences_related_videos_label": "Sýna tengd myndskeið? ", "preferences_annotations_label": "Á að sýna glósur sjálfgefið? ", "preferences_category_visual": "Sjónrænar stillingar", - "preferences_player_style_label": "Spilara stíl: ", - "Dark mode: ": "Myrkur ham: ", + "preferences_player_style_label": "Stíll spilara: ", + "Dark mode: ": "Dökkur hamur: ", "preferences_dark_mode_label": "Þema: ", - "dark": "dimmt", + "dark": "dökkt", "light": "ljóst", - "preferences_thin_mode_label": "Þunnt ham: ", + "preferences_thin_mode_label": "Grannur hamur: ", "preferences_category_subscription": "Áskriftarstillingar", "preferences_annotations_subscribed_label": "Á að sýna glósur sjálfgefið fyrir áskriftarrásir? ", - "Redirect homepage to feed: ": "Endurbeina heimasíðu að straumi: ", - "preferences_max_results_label": "Fjöldi myndbanda sem sýndir eru í straumi: ", - "preferences_sort_label": "Raða myndbönd eftir: ", + "Redirect homepage to feed: ": "Endurbeina heimasíðu að streymi: ", + "preferences_max_results_label": "Fjöldi myndskeiða sem sýnd eru í streymi: ", + "preferences_sort_label": "Raða myndskeiðum eftir: ", "published": "birt", "published - reverse": "birt - afturábak", "alphabetically": "í stafrófsröð", @@ -88,31 +88,31 @@ "`x` uploaded a video": "`x` hlóð upp myndband", "`x` is live": "`x` er í beinni", "preferences_category_data": "Gagnastillingar", - "Clear watch history": "Hreinsa áhorfssögu", + "Clear watch history": "Hreinsa áhorfsferil", "Import/export data": "Flytja inn/út gögn", "Change password": "Breyta lykilorði", - "Manage subscriptions": "Stjórna áskriftum", - "Manage tokens": "Stjórna tákn", - "Watch history": "Áhorfssögu", + "Manage subscriptions": "Sýsla með áskriftir", + "Manage tokens": "Sýsla með teikn", + "Watch history": "Áhorfsferill", "Delete account": "Eyða reikningi", "preferences_category_admin": "Kjörstillingar stjórnanda", "preferences_default_home_label": "Sjálfgefin heimasíða: ", - "preferences_feed_menu_label": "Straum valmynd: ", - "Top enabled: ": "Toppur virkur? ", + "preferences_feed_menu_label": "Streymisvalmynd: ", + "Top enabled: ": "Vinsælast virkt? ", "CAPTCHA enabled: ": "CAPTCHA virk? ", "Login enabled: ": "Innskráning virk? ", "Registration enabled: ": "Nýskráning virkjuð? ", - "Report statistics: ": "Skrá talnagögn? ", + "Report statistics: ": "Skrá tölfræði? ", "Save preferences": "Vista stillingar", "Subscription manager": "Áskriftarstjóri", - "Token manager": "Táknstjóri", - "Token": "Tákn", + "Token manager": "Teiknastjórnun", + "Token": "Teikn", "Import/export": "Flytja inn/út", "unsubscribe": "afskrá", "revoke": "afturkalla", "Subscriptions": "Áskriftir", "search": "leita", - "Log out": "Útskrá", + "Log out": "Skrá út", "Source available here.": "Frumkóði aðgengilegur hér.", "View JavaScript license information.": "Skoða JavaScript leyfisupplýsingar.", "View privacy policy.": "Skoða meðferð persónuupplýsinga.", @@ -122,13 +122,13 @@ "Private": "Einka", "View all playlists": "Skoða alla spilunarlista", "Updated `x` ago": "Uppfært `x` síðann", - "Delete playlist `x`?": "Eiða spilunarlista `x`?", - "Delete playlist": "Eiða spilunarlista", + "Delete playlist `x`?": "Eyða spilunarlista `x`?", + "Delete playlist": "Eyða spilunarlista", "Create playlist": "Búa til spilunarlista", "Title": "Titill", - "Playlist privacy": "Spilunarlista opinberri", - "Editing playlist `x`": "Að breyta spilunarlista `x`", - "Watch on YouTube": "Horfa á YouTube", + "Playlist privacy": "Friðhelgi spilunarlista", + "Editing playlist `x`": "Breyti spilunarlista `x`", + "Watch on YouTube": "Skoða á YouTube", "Hide annotations": "Fela glósur", "Show annotations": "Sýna glósur", "Genre: ": "Tegund: ", @@ -160,26 +160,26 @@ "Wrong username or password": "Rangt notandanafn eða lykilorð", "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", - "Invidious Private Feed for `x`": "Invidious Persónulegur Straumur fyrir `x`", + "Please log in": "Skráðu þig inn", + "Invidious Private Feed for `x`": "Persónulegt Invidious-streymi fyrir `x`", "channel:`x`": "rás:`x`", "Deleted or invalid channel": "Eytt eða ógild rás", "This channel does not exist.": "Þessi rás er ekki til.", - "Could not get channel info.": "Ekki tókst að fá rásarupplýsingar.", + "Could not get channel info.": "Ekki tókst að fá upplýsingar um rásina.", "Could not fetch comments": "Ekki tókst að sækja ummæli", "`x` ago": "`x` síðan", "Load more": "Hlaða meira", "Could not create mix.": "Ekki tókst að búa til blöndu.", "Empty playlist": "Tómur spilunarlisti", - "Not a playlist.": "Ekki spilunarlisti.", + "Not a playlist.": "Er ekki spilunarlisti.", "Playlist does not exist.": "Spilunarlisti er ekki til.", "Could not pull trending pages.": "Ekki tókst að draga vinsælar síður.", "Hidden field \"challenge\" is a required field": "Falinn reitur \"áskorun\" er nauðsynlegur reitur", - "Hidden field \"token\" is a required field": "Falinn reitur \"tákn\" er nauðsynlegur reitur", + "Hidden field \"token\" is a required field": "Falinn reitur \"teikn\" er nauðsynlegur reitur", "Erroneous challenge": "Röng áskorun", - "Erroneous token": "Rangt tákn", + "Erroneous token": "Rangt teikn", "No such user": "Enginn slíkur notandi", - "Token is expired, please try again": "Tákn er útrunnið, vinsamlegast reyndu aftur", + "Token is expired, please try again": "Teiknið er útrunnið, reyndu aftur", "English": "Enska", "English (auto-generated)": "Enska (sjálfkrafa)", "Afrikaans": "Afríkanska", @@ -267,14 +267,14 @@ "Somali": "Sómalska", "Southern Sotho": "Suður Sótó", "Spanish": "Spænska", - "Spanish (Latin America)": "Spænska (Rómönsku Ameríka)", + "Spanish (Latin America)": "Spænska (Rómanska Ameríka)", "Sundanese": "Sundaneska", "Swahili": "Svahílí", "Swedish": "Sænska", "Tajik": "Tadsikíska", "Tamil": "Tamílska", "Telugu": "Telúgú", - "Thai": "Taílenska", + "Thai": "Tælenska", "Turkish": "Tyrkneska", "Ukrainian": "Úkraníska", "Urdu": "Úrdú", @@ -286,9 +286,9 @@ "Yiddish": "Jiddíska", "Yoruba": "Jórúba", "Zulu": "Zúlú", - "Fallback comments: ": "Vara ummæli: ", + "Fallback comments: ": "Ummæli til vara: ", "Popular": "Vinsælt", - "Top": "Topp", + "Top": "Vinsælast", "About": "Um", "Rating: ": "Einkunn: ", "preferences_locale_label": "Tungumál: ", @@ -307,9 +307,194 @@ "`x` marked it with a ❤": "`x` merkti það með ❤", "Audio mode": "Hljóð ham", "Video mode": "Myndband ham", - "channel_tab_videos_label": "Myndbönd", + "channel_tab_videos_label": "Myndskeið", "Playlists": "Spilunarlistar", "channel_tab_community_label": "Samfélag", "Current version: ": "Núverandi útgáfa: ", - "preferences_watch_history_label": "Virkja áhorfssögu: " + "preferences_watch_history_label": "Virkja áhorfsferil: ", + "Chinese (China)": "Kínverska (Kína)", + "Turkish (auto-generated)": "Tyrkneska (sjálfvirkt útbúið)", + "Search": "Leita", + "preferences_save_player_pos_label": "Vista staðsetningu í afspilun: ", + "Popular enabled: ": "Vinsælt virkjað: ", + "search_filters_features_option_purchased": "Keypt", + "Standard YouTube license": "Staðlað YouTube-notkunarleyfi", + "French (auto-generated)": "Franska (sjálfvirkt útbúið)", + "Spanish (Spain)": "Spænska (Spánn)", + "search_filters_title": "Síur", + "search_filters_date_label": "Dags. innsendingar", + "search_filters_features_option_four_k": "4K", + "search_filters_features_option_hd": "HD", + "crash_page_read_the_faq": "lesið Algengar spurningar (FAQ)", + "Add to playlist": "Bæta á spilunarlista", + "Add to playlist: ": "Bæta á spilunarlista: ", + "Answer": "Svar", + "Search for videos": "Leita að myndskeiðum", + "generic_channels_count": "{{count}} rás", + "generic_channels_count_plural": "{{count}} rásir", + "generic_videos_count": "{{count}} myndskeið", + "generic_videos_count_plural": "{{count}} myndskeið", + "The Popular feed has been disabled by the administrator.": "Kerfisstjórinn hefur gert Vinsælt-streymið óvirkt.", + "generic_playlists_count": "{{count}} spilunarlisti", + "generic_playlists_count_plural": "{{count}} spilunarlistar", + "generic_subscribers_count": "{{count}} áskrifandi", + "generic_subscribers_count_plural": "{{count}} áskrifendur", + "generic_subscriptions_count": "{{count}} áskrift", + "generic_subscriptions_count_plural": "{{count}} áskriftir", + "generic_button_delete": "Eyða", + "Import YouTube watch history (.json)": "Flytja inn YouTube áhorfsferil (.json)", + "preferences_vr_mode_label": "Gagnvirk 360 gráðu myndskeið (krefst WebGL): ", + "preferences_quality_dash_option_auto": "Sjálfvirkt", + "preferences_quality_dash_option_best": "Best", + "preferences_quality_dash_option_worst": "Verst", + "preferences_quality_dash_label": "Æskileg DASH-gæði myndmerkis: ", + "preferences_extend_desc_label": "Sjálfvirkt útvíkka lýsingu á myndskeiði: ", + "preferences_region_label": "Land efnis: ", + "preferences_show_nick_label": "Birta gælunafn efst: ", + "tokens_count": "{{count}} teikn", + "tokens_count_plural": "{{count}} teikn", + "subscriptions_unseen_notifs_count": "{{count}} óskoðuð tilkynning", + "subscriptions_unseen_notifs_count_plural": "{{count}} óskoðaðar tilkynningar", + "Released under the AGPLv3 on Github.": "Gefið út með AGPLv3-notkunarleyfi á GitHub.", + "Music in this video": "Tónlist í þessu myndskeiði", + "Artist: ": "Flytjandi: ", + "Album: ": "Hljómplata: ", + "comments_view_x_replies": "Skoða {{count}} svar", + "comments_view_x_replies_plural": "Skoða {{count}} svör", + "comments_points_count": "{{count}} punktur", + "comments_points_count_plural": "{{count}} punktar", + "Cantonese (Hong Kong)": "Kantónska (Hong Kong)", + "Chinese": "Kínverska", + "Chinese (Hong Kong)": "Kínverska (Hong Kong)", + "Chinese (Taiwan)": "Kínverska (Taívan)", + "Japanese (auto-generated)": "Japanska (sjálfvirkt útbúið)", + "generic_count_minutes": "{{count}} mínúta", + "generic_count_minutes_plural": "{{count}} mínútur", + "generic_count_seconds": "{{count}} sekúnda", + "generic_count_seconds_plural": "{{count}} sekúndur", + "search_filters_date_option_hour": "Síðustu klukkustund", + "search_filters_apply_button": "Virkja valdar síur", + "next_steps_error_message_go_to_youtube": "Fara á YouTube", + "footer_original_source_code": "Upprunalegur grunnkóði", + "videoinfo_started_streaming_x_ago": "Byrjaði streymi fyrir `x` síðan", + "next_steps_error_message": "Á eftir þessu ættirðu að prófa: ", + "videoinfo_invidious_embed_link": "Ívefja tengil", + "download_subtitles": "Skjátextar - `x` (.vtt)", + "user_created_playlists": "`x` útbjó spilunarlista", + "user_saved_playlists": "`x` vistaði spilunarlista", + "Video unavailable": "Myndskeið ekki tiltækt", + "videoinfo_watch_on_youTube": "Skoða á YouTube", + "crash_page_you_found_a_bug": "Það lítur út eins og þú hafir fundið galla í Invidious!", + "crash_page_before_reporting": "Áður en þú tilkynnir villu, gakktu úr skugga um að þú hafir:", + "crash_page_switch_instance": "reynt að nota annað tilvik", + "crash_page_report_issue": "Ef ekkert af ofantöldu hjálpaði, ættirðu að opna nýja verkbeiðni (issue) á GitHub (helst á ensku) og láta fylgja eftirfarandi texta í skilaboðunum þínum (alls EKKI þýða þennan texta):", + "channel_tab_shorts_label": "Stuttmyndir", + "carousel_slide": "Skyggna {{current}} af {{total}}", + "carousel_go_to": "Fara á skyggnu `x`", + "channel_tab_streams_label": "Bein streymi", + "channel_tab_playlists_label": "Spilunarlistar", + "toggle_theme": "Víxla þema", + "carousel_skip": "Sleppa hringekjunni", + "preferences_quality_option_medium": "Miðlungs", + "search_message_use_another_instance": " Þú getur líka leitað á öðrum netþjóni.", + "footer_source_code": "Grunnkóði", + "English (United Kingdom)": "Enska (Bretland)", + "English (United States)": "Enska (Bandarísk)", + "Vietnamese (auto-generated)": "Víetnamska (sjálfvirkt útbúið)", + "generic_count_months": "{{count}} mánuður", + "generic_count_months_plural": "{{count}} mánuðir", + "search_filters_sort_option_rating": "Einkunn", + "videoinfo_youTube_embed_link": "Ívefja", + "error_video_not_in_playlist": "Umbeðið myndskeið fyrirfinnst ekki í þessum spilunarlista. Smelltu hér til að fara á heimasíðu spilunarlistans.", + "generic_views_count": "{{count}} áhorf", + "generic_views_count_plural": "{{count}} áhorf", + "playlist_button_add_items": "Bæta við myndskeiðum", + "Show more": "Sýna meira", + "Show less": "Sýna minna", + "Song: ": "Lag: ", + "channel_tab_podcasts_label": "Hlaðvörp (podcasts)", + "channel_tab_releases_label": "Útgáfur", + "Download is disabled": "Niðurhal er óvirkt", + "search_filters_features_option_location": "Staðsetning", + "preferences_quality_dash_option_720p": "720p", + "Switch Invidious Instance": "Skipta um Invidious-tilvik", + "search_message_no_results": "Engar niðurstöður fundust.", + "search_message_change_filters_or_query": "Reyndu að víkka leitarsviðið og/eða breyta síunum.", + "Dutch (auto-generated)": "Hollenska (sjálfvirkt útbúið)", + "German (auto-generated)": "Þýska (sjálfvirkt útbúið)", + "Indonesian (auto-generated)": "Indónesíska (sjálfvirkt útbúið)", + "Interlingue": "Interlingue", + "Italian (auto-generated)": "Ítalska (sjálfvirkt útbúið)", + "Russian (auto-generated)": "Rússneska (sjálfvirkt útbúið)", + "Spanish (auto-generated)": "Spænska (sjálfvirkt útbúið)", + "Spanish (Mexico)": "Spænska (Mexíkó)", + "generic_count_hours": "{{count}} klukkustund", + "generic_count_hours_plural": "{{count}} klukkustundir", + "generic_count_years": "{{count}} ár", + "generic_count_years_plural": "{{count}} ár", + "generic_count_weeks": "{{count}} vika", + "generic_count_weeks_plural": "{{count}} vikur", + "search_filters_date_option_none": "Hvaða dagsetning sem er", + "Channel Sponsor": "Styrktaraðili rásar", + "search_filters_date_option_week": "Í þessari viku", + "search_filters_date_option_month": "Í þessum mánuði", + "search_filters_date_option_year": "Á þessu ári", + "search_filters_type_option_playlist": "Spilunarlisti", + "search_filters_type_option_show": "Þáttur", + "search_filters_duration_label": "Tímalengd", + "search_filters_duration_option_long": "Langt (> 20 mínútur)", + "search_filters_features_option_live": "Beint", + "search_filters_features_option_three_sixty": "360°", + "search_filters_features_option_vr180": "VR180", + "search_filters_features_option_three_d": "3D", + "search_filters_features_option_hdr": "HDR", + "search_filters_sort_label": "Raða eftir", + "search_filters_sort_option_relevance": "Samsvörun", + "footer_donate_page": "Styrkja", + "footer_modfied_source_code": "Breyttur grunnkóði", + "crash_page_refresh": "reynt að endurlesa síðuna", + "crash_page_search_issue": "leitað að fyrirliggjandi villum á GitHub", + "none": "ekkert", + "adminprefs_modified_source_code_url_label": "Slóð á gagnasafn með breyttum grunnkóða", + "preferences_quality_option_hd720": "HD720", + "preferences_quality_option_small": "Lítið", + "preferences_category_misc": "Ýmsar kjörstillingar", + "preferences_automatic_instance_redirect_label": "Sjálfvirk endurbeining tilvika (farið til vara á redirect.invidious.io): ", + "Portuguese (auto-generated)": "Portúgalska (sjálfvirkt útbúið)", + "Portuguese (Brazil)": "Portúgalska (Brasilía)", + "generic_button_edit": "Breyta", + "generic_button_save": "Vista", + "generic_button_cancel": "Hætta við", + "generic_button_rss": "RSS", + "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_480p": "480p", + "preferences_quality_dash_option_360p": "360p", + "preferences_quality_dash_option_240p": "240p", + "preferences_quality_dash_option_144p": "144p", + "invidious": "Invidious", + "Korean (auto-generated)": "Kóreska (sjálfvirkt útbúið)", + "generic_count_days": "{{count}} dagur", + "generic_count_days_plural": "{{count}} dagar", + "search_filters_date_option_today": "Í dag", + "search_filters_type_label": "Tegund", + "search_filters_type_option_all": "Hvaða tegund sem er", + "search_filters_type_option_video": "Myndskeið", + "search_filters_type_option_channel": "Rás", + "search_filters_type_option_movie": "Kvikmynd", + "search_filters_duration_option_none": "Hvaða lengd sem er", + "search_filters_duration_option_short": "Stutt (< 4 mínútur)", + "search_filters_duration_option_medium": "Miðlungs (4 - 20 mínútur)", + "search_filters_features_label": "Eiginleikar", + "search_filters_features_option_subtitles": "Skjátextar/CC", + "search_filters_features_option_c_commons": "Creative Commons", + "search_filters_sort_option_date": "Dags. innsendingar", + "search_filters_sort_option_views": "Fjöldi áhorfa", + "next_steps_error_message_refresh": "Endurlesa", + "footer_documentation": "Leiðbeiningar", + "channel_tab_channels_label": "Rásir", + "Import YouTube playlist (.csv)": "Flytja inn YouTube spilunarlista (.csv)", + "preferences_quality_option_dash": "DASH (aðlaganleg gæði)" } From 366732b4fdba45f0c34eb14b45a178f4baf18b89 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 13 Aug 2024 19:51:36 +0200 Subject: [PATCH 144/155] Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Update Bulgarian translation Update German translation Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Co-authored-by: Hosted Weblate Co-authored-by: Jose Delvani Co-authored-by: Least Significant Bite Co-authored-by: NEXI Co-authored-by: Radoslav Lelchev Co-authored-by: Random Co-authored-by: Unacceptium Co-authored-by: hiatsu0 --- locales/hu-HU.json | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/locales/hu-HU.json b/locales/hu-HU.json index 1899b71c..8fbdd82f 100644 --- a/locales/hu-HU.json +++ b/locales/hu-HU.json @@ -464,5 +464,23 @@ "search_filters_features_option_vr180": "180°-os virtuális valóság", "search_filters_apply_button": "Keresés a megadott szűrőkkel", "Popular enabled: ": "Népszerű engedélyezve ", - "error_video_not_in_playlist": "A lejátszási listában keresett videó nem létezik. Kattintson ide a lejátszási listához jutáshoz." + "error_video_not_in_playlist": "A lejátszási listában keresett videó nem létezik. Kattintson ide a lejátszási listához jutáshoz.", + "generic_button_delete": "Törlés", + "generic_button_rss": "RSS", + "Import YouTube playlist (.csv)": "Youtube lejátszási lista (.csv) importálása", + "Standard YouTube license": "Alap YouTube-licensz", + "Add to playlist": "Hozzáadás lejátszási listához", + "Add to playlist: ": "Hozzáadás a lejátszási listához: ", + "Answer": "Válasz", + "Search for videos": "Keresés videókhoz", + "generic_channels_count": "{{count}} csatorna", + "generic_channels_count_plural": "{{count}} csatornák", + "generic_button_edit": "Szerkesztés", + "generic_button_save": "Mentés", + "generic_button_cancel": "Mégsem", + "playlist_button_add_items": "Videók hozzáadása", + "Music in this video": "Zene ezen videóban", + "Song: ": "Dal: ", + "Album: ": "Album: ", + "Import YouTube watch history (.json)": "Youtube megtekintési előzmények (.json) importálása" } From 8ad19f06ee1c07cf35fdd1442af9796bbb632297 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 13 Aug 2024 19:51:36 +0200 Subject: [PATCH 145/155] Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Update Bulgarian translation Update German translation Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Co-authored-by: Hosted Weblate Co-authored-by: Jose Delvani Co-authored-by: Least Significant Bite Co-authored-by: NEXI Co-authored-by: Radoslav Lelchev Co-authored-by: Random Co-authored-by: Unacceptium Co-authored-by: hiatsu0 --- locales/it.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/it.json b/locales/it.json index 79aa6c16..46d7ef13 100644 --- a/locales/it.json +++ b/locales/it.json @@ -30,7 +30,7 @@ "Import and Export Data": "Importazione ed esportazione dati", "Import": "Importa", "Import Invidious data": "Importa dati Invidious in formato JSON", - "Import YouTube subscriptions": "Importa le iscrizioni da YouTube/OPML", + "Import YouTube subscriptions": "Importa iscrizioni in CSV o OPML di YouTube", "Import FreeTube subscriptions (.db)": "Importa le iscrizioni da FreeTube (.db)", "Import NewPipe subscriptions (.json)": "Importa le iscrizioni da NewPipe (.json)", "Import NewPipe data (.zip)": "Importa i dati di NewPipe (.zip)", From e538410262acc7598dce7ded93d9b4442f19a360 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 13 Aug 2024 19:51:36 +0200 Subject: [PATCH 146/155] Update Dutch translation Update Dutch translation Co-authored-by: Dick Groskamp Co-authored-by: Hosted Weblate Co-authored-by: Martijn Westerink --- locales/nl.json | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/locales/nl.json b/locales/nl.json index d495a2d1..26e35e99 100644 --- a/locales/nl.json +++ b/locales/nl.json @@ -21,7 +21,7 @@ "Import and Export Data": "Gegevens im- en exporteren", "Import": "Importeren", "Import Invidious data": "JSON-gegevens Invidious importeren", - "Import YouTube subscriptions": "YouTube-/OPML-abonnementen importeren", + "Import YouTube subscriptions": "YouTube CVS of OPML-abonnementen importeren", "Import FreeTube subscriptions (.db)": "FreeTube-abonnementen importeren (.db)", "Import NewPipe subscriptions (.json)": "NewPipe-abonnementen importeren (.json)", "Import NewPipe data (.zip)": "NewPipe-gegevens importeren (.zip)", @@ -86,7 +86,7 @@ "Only show latest unwatched video from channel: ": "Alleen nieuwste niet-bekeken video van kanaal tonen: ", "preferences_unseen_only_label": "Alleen niet-bekeken videos tonen: ", "preferences_notifications_only_label": "Alleen meldingen tonen (als die er zijn): ", - "Enable web notifications": "Systemmeldingen inschakelen", + "Enable web notifications": "Systeemmeldingen inschakelen", "`x` uploaded a video": "`x` heeft een video geüpload", "`x` is live": "`x` zendt nu live uit", "preferences_category_data": "Gegevensinstellingen", @@ -192,15 +192,15 @@ "Arabic": "Arabisch", "Armenian": "Armeens", "Azerbaijani": "Azerbeidzjaans", - "Bangla": "Bangla", + "Bangla": "Bengaals", "Basque": "Baskisch", - "Belarusian": "Wit-Rrussisch", + "Belarusian": "Wit-Russisch", "Bosnian": "Bosnisch", "Bulgarian": "Bulgaars", "Burmese": "Birmaans", "Catalan": "Catalaans", - "Cebuano": "Cebuano", - "Chinese (Simplified)": "Chinees (Veereenvoudigd)", + "Cebuano": "Cebuaans", + "Chinese (Simplified)": "Chinees (Vereenvoudigd)", "Chinese (Traditional)": "Chinees (Traditioneel)", "Corsican": "Corsicaans", "Croatian": "Kroatisch", @@ -217,23 +217,23 @@ "German": "Duits", "Greek": "Grieks", "Gujarati": "Gujarati", - "Haitian Creole": "Creools", + "Haitian Creole": "Haïtiaans Creools", "Hausa": "Hausa", "Hawaiian": "Hawaïaans", - "Hebrew": "Heebreeuws", + "Hebrew": "Hebreeuws", "Hindi": "Hindi", "Hmong": "Hmong", "Hungarian": "Hongaars", "Icelandic": "IJslands", - "Igbo": "Igbo", + "Igbo": "Ikbo", "Indonesian": "Indonesisch", "Irish": "Iers", "Italian": "Italiaans", "Japanese": "Japans", "Javanese": "Javaans", - "Kannada": "Kannada", + "Kannada": "Kannada-taal", "Kazakh": "Kazachs", - "Khmer": "Khmer", + "Khmer": "Khmer-taal", "Korean": "Koreaans", "Kurdish": "Koerdisch", "Kyrgyz": "Kirgizisch", @@ -245,10 +245,10 @@ "Macedonian": "Macedonisch", "Malagasy": "Malagassisch", "Malay": "Maleisisch", - "Malayalam": "Malayalam", + "Malayalam": "Malayalam-taal", "Maltese": "Maltees", "Maori": "Maorisch", - "Marathi": "Marathi", + "Marathi": "Marathi-taal", "Mongolian": "Mongools", "Nepali": "Nepalees", "Norwegian Bokmål": "Noors (Bokmål)", @@ -309,7 +309,7 @@ "(edited)": "(bewerkt)", "YouTube comment permalink": "Link naar YouTube-reactie", "permalink": "permalink", - "`x` marked it with a ❤": "`x` heeft dit gemarkeerd met ❤", + "`x` marked it with a ❤": "`x` heeft dit gemarkeerd met een ❤", "Audio mode": "Audiomodus", "Video mode": "Videomodus", "channel_tab_videos_label": "Video's", @@ -396,7 +396,7 @@ "Dutch (auto-generated)": "Nederlands (automatisch gegenereerd)", "tokens_count": "{{count}} token", "tokens_count_plural": "{{count}} tokens", - "generic_count_seconds": "{{count}} second", + "generic_count_seconds": "{{count}} seconde", "generic_count_seconds_plural": "{{count}} seconden", "generic_count_weeks": "{{count}} week", "generic_count_weeks_plural": "{{count}} weken", @@ -449,7 +449,7 @@ "generic_playlists_count_plural": "{{count}} afspeellijsten", "Chinese (Hong Kong)": "Chinees (Hongkong)", "Korean (auto-generated)": "Koreaans (automatisch gegenereerd)", - "search_filters_apply_button": "Geselecteerd filters toepassen", + "search_filters_apply_button": "Geselecteerde filters toepassen", "search_message_use_another_instance": " Je kan ook zoeken op een andere instantie.", "Cantonese (Hong Kong)": "Kantonees (Hongkong)", "Chinese (China)": "Chinees (China)", From ae93146f473248590ccdd96cb2229e09c94d4a6c Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 13 Aug 2024 19:51:36 +0200 Subject: [PATCH 147/155] Update French translation Update French translation Update French translation Update French translation Co-authored-by: ABCraft19 Co-authored-by: Duc-Thomas Co-authored-by: Hosted Weblate Co-authored-by: Patricio Carrau Co-authored-by: Samantaz Fox --- locales/fr.json | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index 251e88bc..3bcc9014 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -18,7 +18,7 @@ "generic_subscriptions_count_1": "{{count}} d'abonnements", "generic_subscriptions_count_2": "{{count}} abonnements", "generic_button_delete": "Supprimer", - "generic_button_edit": "Editer", + "generic_button_edit": "Modifier", "generic_button_save": "Enregistrer", "generic_button_cancel": "Annuler", "generic_button_rss": "RSS", @@ -44,7 +44,7 @@ "Import and Export Data": "Importer et exporter des données", "Import": "Importer", "Import Invidious data": "Importer des données Invidious au format JSON", - "Import YouTube subscriptions": "Importer des abonnements YouTube/OPML", + "Import YouTube subscriptions": "Importer des abonnements YouTube aux formats OPML/CSV", "Import FreeTube subscriptions (.db)": "Importer des abonnements FreeTube (.db)", "Import NewPipe subscriptions (.json)": "Importer des abonnements NewPipe (.json)", "Import NewPipe data (.zip)": "Importer des données NewPipe (.zip)", @@ -504,5 +504,14 @@ "Import YouTube playlist (.csv)": "Importer des listes de lecture de Youtube (.csv)", "channel_tab_releases_label": "Parutions", "channel_tab_podcasts_label": "Émissions audio", - "Import YouTube watch history (.json)": "Importer l'historique de visionnement YouTube (.json)" + "Import YouTube watch history (.json)": "Importer l'historique de visionnement YouTube (.json)", + "Add to playlist: ": "Ajouter à la playlist : ", + "Add to playlist": "Ajouter à la playlist", + "Answer": "Répondre", + "Search for videos": "Rechercher des vidéos", + "The Popular feed has been disabled by the administrator.": "Le flux populaire a été désactivé par l'administrateur.", + "carousel_skip": "Passez le carrousel", + "carousel_slide": "Diapositive {{current}} sur {{total}}", + "carousel_go_to": "Aller à la diapositive `x`", + "toggle_theme": "Changer le Thème" } From 86ec5ad6e0e9da69d6308a73cb89de6710dab873 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 13 Aug 2024 19:51:36 +0200 Subject: [PATCH 148/155] Update Swedish translation Co-authored-by: Hosted Weblate Co-authored-by: bittin1ddc447d824349b2 --- locales/sv-SE.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/sv-SE.json b/locales/sv-SE.json index 76edc341..b2f0fd17 100644 --- a/locales/sv-SE.json +++ b/locales/sv-SE.json @@ -21,7 +21,7 @@ "Import and Export Data": "Importera och exportera data", "Import": "Importera", "Import Invidious data": "Importera Invidious JSON data", - "Import YouTube subscriptions": "Importera YouTube/OPML prenumerationer", + "Import YouTube subscriptions": "Importera YouTube CSV eller 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)", From f837d99eabbfc7b6c56f2ae3d22975b8517c95ba Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 13 Aug 2024 19:51:36 +0200 Subject: [PATCH 149/155] Update Persian translation Co-authored-by: Hosted Weblate Co-authored-by: Wireless Acquired --- locales/fa.json | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/locales/fa.json b/locales/fa.json index d0251201..6723aad8 100644 --- a/locales/fa.json +++ b/locales/fa.json @@ -17,7 +17,7 @@ "View playlist on YouTube": "دیدن فهرست پخش در یوتیوب", "newest": "تازه‌ترین", "oldest": "کهنه‌ترین", - "popular": "محبوب", + "popular": "پرطرفدار", "last": "آخرین", "Next page": "صفحه بعد", "Previous page": "صفحه قبل", @@ -31,7 +31,7 @@ "Import and Export Data": "درون‌برد و برون‌برد داده", "Import": "درون‌برد", "Import Invidious data": "وارد کردن داده JSON اینویدیوس", - "Import YouTube subscriptions": "وارد کردن اشتراک OPML/ یوتیوب", + "Import YouTube subscriptions": "وارد کردن فایل CSV یا OPML سابسکرایب های یوتیوب", "Import FreeTube subscriptions (.db)": "درون‌برد اشتراک‌های فری‌تیوب (.db)", "Import NewPipe subscriptions (.json)": "درون‌برد اشتراک‌های نیوپایپ (.json)", "Import NewPipe data (.zip)": "درون‌برد داده نیوپایپ (.zip)", @@ -328,7 +328,7 @@ "generic_count_seconds": "{{count}} ثانیه", "generic_count_seconds_plural": "{{count}} ثانیه", "Fallback comments: ": "نظرات عقب گرد: ", - "Popular": "محبوب", + "Popular": "پربیننده", "Search": "جست و جو", "Top": "بالا", "About": "درباره", @@ -484,5 +484,17 @@ "channel_tab_shorts_label": "Shortها", "channel_tab_playlists_label": "فهرست‌های پخش", "channel_tab_channels_label": "کانال‌ها", - "error_video_not_in_playlist": "ویدیوی درخواستی معلق به این فهرست پخش نیست. کلیک کنید تا به صفحهٔ اصلی فهرست پخش بروید." + "error_video_not_in_playlist": "ویدیوی درخواستی معلق به این فهرست پخش نیست. کلیک کنید تا به صفحهٔ اصلی فهرست پخش بروید.", + "Add to playlist": "به لیست پخش افزوده شود", + "Answer": "پاسخ", + "Search for videos": "جست و جو برای ویدیوها", + "Add to playlist: ": "افزودن به لیست پخش ", + "The Popular feed has been disabled by the administrator.": "بخش ویدیوهای پرطرفدار توسط مدیر غیرفعال شده است.", + "carousel_slide": "اسلاید {{current}} از {{total}}", + "carousel_skip": "رد شدن از گرداننده", + "carousel_go_to": "به اسلاید `x` برو", + "crash_page_search_issue": "دنبال گشتیم بین مشکلات در گیت هاب ", + "crash_page_report_issue": "اگر هیچ یک از روش های بالا کمکی نکردند لطفا (ترجیحا به انگلیسی) یک سوال جدید در گیت هاب بپرسید و طوری که سوالتون شامل متن زیر باشه:", + "channel_tab_releases_label": "آثار", + "toggle_theme": "تغییر وضعیت تم" } From 905fed66d1faa594f8e503aabe1e16680e82c72a Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 13 Aug 2024 19:51:36 +0200 Subject: [PATCH 150/155] Update Finnish translation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update Finnish translation Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Update Bulgarian translation Update German translation Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Co-authored-by: Hosted Weblate Co-authored-by: Jiri Grönroos Co-authored-by: Jose Delvani Co-authored-by: Least Significant Bite Co-authored-by: NEXI Co-authored-by: Radoslav Lelchev Co-authored-by: Random Co-authored-by: Tuomas Hietala Co-authored-by: Unacceptium Co-authored-by: hiatsu0 --- locales/fi.json | 120 ++++++++++++++++++++++++++++++------------------ 1 file changed, 76 insertions(+), 44 deletions(-) diff --git a/locales/fi.json b/locales/fi.json index 14c2b0fc..b0df1e46 100644 --- a/locales/fi.json +++ b/locales/fi.json @@ -28,7 +28,7 @@ "Export": "Vie", "Export subscriptions as OPML": "Vie tilaukset OPML-muodossa", "Export subscriptions as OPML (for NewPipe & FreeTube)": "Vie tilaukset OPML-muodossa (NewPipe & FreeTube)", - "Export data as JSON": "Vie Invidious-data JSON-muodossa", + "Export data as JSON": "Vie Invidiousin tiedot JSON-muodossa", "Delete account?": "Poista tili?", "History": "Historia", "An alternative front-end to YouTube": "Vaihtoehtoinen front-end YouTubelle", @@ -46,12 +46,12 @@ "E-mail": "Sähköposti", "Preferences": "Asetukset", "preferences_category_player": "Soittimen asetukset", - "preferences_video_loop_label": "Toista jatkuvasti aina: ", - "preferences_autoplay_label": "Automaattinen toisto: ", + "preferences_video_loop_label": "Toista aina uudelleen: ", + "preferences_autoplay_label": "Automaattinen toiston aloitus: ", "preferences_continue_label": "Toista seuraava oletuksena: ", - "preferences_continue_autoplay_label": "Toista seuraava video automaattisesti: ", + "preferences_continue_autoplay_label": "Aloita seuraava video automaattisesti: ", "preferences_listen_label": "Kuuntele oletuksena: ", - "preferences_local_label": "Proxytä videot: ", + "preferences_local_label": "Videot välityspalvelimen kautta: ", "preferences_speed_label": "Oletusnopeus: ", "preferences_quality_label": "Ensisijainen videon laatu: ", "preferences_volume_label": "Soittimen äänenvoimakkuus: ", @@ -63,7 +63,7 @@ "preferences_related_videos_label": "Näytä aiheeseen liittyviä videoita: ", "preferences_annotations_label": "Näytä huomautukset oletuksena: ", "preferences_extend_desc_label": "Laajenna automaattisesti videon kuvausta: ", - "preferences_vr_mode_label": "Interaktiiviset 360-asteiset videot (vaatii WebGL:n): ", + "preferences_vr_mode_label": "Interaktiiviset 360-videot (vaatii WebGL:n): ", "preferences_category_visual": "Visuaaliset asetukset", "preferences_player_style_label": "Soittimen tyyli: ", "Dark mode: ": "Tumma tila: ", @@ -137,9 +137,9 @@ "Show less": "Näytä vähemmän", "Watch on YouTube": "Katso YouTubessa", "Switch Invidious Instance": "Vaihda Invidious-instanssia", - "Hide annotations": "Piilota merkkaukset", - "Show annotations": "Näytä merkkaukset", - "Genre: ": "Genre: ", + "Hide annotations": "Piilota huomautukset", + "Show annotations": "Näytä huomautukset", + "Genre: ": "Tyylilaji: ", "License: ": "Lisenssi: ", "Family friendly? ": "Kaiken ikäisille sopiva? ", "Wilson score: ": "Wilson-pistemäärä: ", @@ -168,7 +168,7 @@ "Wrong username or password": "Väärä käyttäjänimi tai salasana", "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ä", + "Please log in": "Kirjaudu sisään", "Invidious Private Feed for `x`": "Invidiousin yksityinen syöte `x`:lle", "channel:`x`": "kanava:`x`", "Deleted or invalid channel": "Poistettu tai virheellinen kanava", @@ -178,7 +178,7 @@ "`x` ago": "`x` sitten", "Load more": "Lataa lisää", "Could not create mix.": "Sekoituksen luominen epäonnistui.", - "Empty playlist": "Tyhjennä soittolista", + "Empty playlist": "Tyhjä soittolista", "Not a playlist.": "Ei ole soittolista.", "Playlist does not exist.": "Soittolistaa ei ole olemassa.", "Could not pull trending pages.": "Nousussa olevien sivujen lataus epäonnistui.", @@ -216,11 +216,11 @@ "Filipino": "filipino", "Finnish": "suomi", "French": "ranska", - "Galician": "galego", + "Galician": "galicia", "Georgian": "georgia", "German": "saksa", "Greek": "kreikka", - "Gujarati": "gujarati", + "Gujarati": "gudžarati", "Haitian Creole": "haitinkreoli", "Hausa": "hausa", "Hawaiian": "havaiji", @@ -327,11 +327,11 @@ "search_filters_duration_label": "Kesto", "search_filters_features_label": "Ominaisuudet", "search_filters_sort_label": "Luokittele", - "search_filters_date_option_hour": "Viimeisin tunti", + "search_filters_date_option_hour": "Tunnin sisään", "search_filters_date_option_today": "Tänään", - "search_filters_date_option_week": "Tämä viikko", - "search_filters_date_option_month": "Tämä kuukausi", - "search_filters_date_option_year": "Tämä vuosi", + "search_filters_date_option_week": "Tällä viikolla", + "search_filters_date_option_month": "Tässä kuussa", + "search_filters_date_option_year": "Tänä vuonna", "search_filters_type_option_video": "Video", "search_filters_type_option_channel": "Kanava", "search_filters_type_option_playlist": "Soittolista", @@ -346,7 +346,7 @@ "search_filters_features_option_location": "Sijainti", "search_filters_features_option_hdr": "HDR", "Current version: ": "Tämänhetkinen versio: ", - "next_steps_error_message": "Sinun tulisi kokeilla seuraavia: ", + "next_steps_error_message": "Kokeile seuraavia: ", "next_steps_error_message_refresh": "Päivitä", "next_steps_error_message_go_to_youtube": "Siirry YouTubeen", "generic_count_hours": "{{count}} tunti", @@ -391,7 +391,7 @@ "subscriptions_unseen_notifs_count": "{{count}} näkemätön ilmoitus", "subscriptions_unseen_notifs_count_plural": "{{count}} näkemätöntä ilmoitusta", "crash_page_switch_instance": "yrittänyt käyttää toista instassia", - "videoinfo_invidious_embed_link": "Upotuslinkki", + "videoinfo_invidious_embed_link": "Upotettava linkki", "user_saved_playlists": "`x` tallennetua soittolistaa", "crash_page_report_issue": "Jos mikään näistä ei auttanut, avaathan uuden issuen GitHubissa (mieluiten englanniksi) ja sisällytät seuraavan tekstin viestissäsi (ÄLÄ käännä tätä tekstiä):", "preferences_quality_option_hd720": "HD720", @@ -410,7 +410,7 @@ "preferences_quality_dash_option_auto": "Auto", "preferences_quality_dash_option_best": "Paras", "preferences_quality_option_dash": "DASH (mukautuva laatu)", - "preferences_quality_dash_label": "Haluttava DASH-videolaatu: ", + "preferences_quality_dash_label": "Ensisijainen DASH-videolaatu: ", "generic_count_years": "{{count}} vuosi", "generic_count_years_plural": "{{count}} vuotta", "search_filters_features_option_purchased": "Ostettu", @@ -421,39 +421,39 @@ "preferences_save_player_pos_label": "Tallenna toistokohta: ", "footer_donate_page": "Lahjoita", "footer_source_code": "Lähdekoodi", - "adminprefs_modified_source_code_url_label": "URL muokattuun lähdekoodirepositoryyn", - "Released under the AGPLv3 on Github.": "Julkaistu AGPLv3-lisenssin alla GitHubissa.", + "adminprefs_modified_source_code_url_label": "URL muokatun lähdekoodin repositorioon", + "Released under the AGPLv3 on Github.": "Julkaistu AGPLv3-lisenssillä GitHubissa.", "search_filters_duration_option_short": "Lyhyt (< 4 minuuttia)", "search_filters_duration_option_long": "Pitkä (> 20 minuuttia)", "footer_documentation": "Dokumentaatio", "footer_original_source_code": "Alkuperäinen lähdekoodi", "footer_modfied_source_code": "Muokattu lähdekoodi", - "Japanese (auto-generated)": "Japani (automaattisesti luotu)", - "German (auto-generated)": "Saksa (automaattisesti luotu)", + "Japanese (auto-generated)": "japani (automaattisesti luotu)", + "German (auto-generated)": "saksa (automaattisesti luotu)", "Portuguese (auto-generated)": "portugali (automaattisesti luotu)", "Russian (auto-generated)": "Venäjä (automaattisesti luotu)", "preferences_watch_history_label": "Ota katseluhistoria käyttöön: ", - "English (United Kingdom)": "Englanti (Iso-Britannia)", - "English (United States)": "Englanti (Yhdysvallat)", - "Cantonese (Hong Kong)": "Kantoninkiina (Hong Kong)", - "Chinese": "Kiina", - "Chinese (China)": "Kiina (Kiina)", - "Chinese (Hong Kong)": "Kiina (Hong Kong)", - "Chinese (Taiwan)": "Kiina (Taiwan)", - "Dutch (auto-generated)": "Hollanti (automaattisesti luotu)", - "French (auto-generated)": "Ranska (automaattisesti luotu)", - "Indonesian (auto-generated)": "Indonesia (automaattisesti luotu)", - "Interlingue": "Interlingue", + "English (United Kingdom)": "englanti (Iso-Britannia)", + "English (United States)": "englanti (Yhdysvallat)", + "Cantonese (Hong Kong)": "kantoninkiina (Hongkong)", + "Chinese": "kiina", + "Chinese (China)": "kiina (Kiina)", + "Chinese (Hong Kong)": "kiina (Hongkong)", + "Chinese (Taiwan)": "kiina (Taiwan)", + "Dutch (auto-generated)": "hollanti (automaattisesti luotu)", + "French (auto-generated)": "ranska (automaattisesti luotu)", + "Indonesian (auto-generated)": "indonesia (automaattisesti luotu)", + "Interlingue": "interlingue", "Italian (auto-generated)": "Italia (automaattisesti luotu)", - "Korean (auto-generated)": "Korea (automaattisesti luotu)", + "Korean (auto-generated)": "korea (automaattisesti luotu)", "Portuguese (Brazil)": "portugali (Brasilia)", - "Spanish (auto-generated)": "Espanja (automaattisesti luotu)", - "Spanish (Mexico)": "Espanja (Meksiko)", - "Spanish (Spain)": "Espanja (Espanja)", - "Turkish (auto-generated)": "Turkki (automaattisesti luotu)", - "Vietnamese (auto-generated)": "Vietnam (automaattisesti luotu)", - "search_filters_title": "Suodatin", - "search_message_no_results": "Ei tuloksia löydetty.", + "Spanish (auto-generated)": "espanja (automaattisesti luotu)", + "Spanish (Mexico)": "espanja (Meksiko)", + "Spanish (Spain)": "espanja (Espanja)", + "Turkish (auto-generated)": "turkki (automaattisesti luotu)", + "Vietnamese (auto-generated)": "vietnam (automaattisesti luotu)", + "search_filters_title": "Suodattimet", + "search_message_no_results": "Tuloksia ei löytynyt.", "search_message_change_filters_or_query": "Yritä hakukyselysi laajentamista ja/tai suodattimien muuttamista.", "search_filters_duration_option_none": "Mikä tahansa kesto", "search_filters_features_option_vr180": "VR180", @@ -464,5 +464,37 @@ "search_filters_date_option_none": "Milloin tahansa", "search_filters_type_option_all": "Mikä tahansa tyyppi", "Popular enabled: ": "Suosittu käytössä: ", - "error_video_not_in_playlist": "Pyydettyä videota ei löydy tästä soittolistasta. Klikkaa tähän päästäksesi soittolistan etusivulle." + "error_video_not_in_playlist": "Pyydettyä videota ei ole tässä soittolistassa. Klikkaa tästä päästäksesi soittolistan kotisivulle.", + "Import YouTube playlist (.csv)": "Tuo YouTube-soittolista (.csv)", + "Music in this video": "Musiikki tässä videossa", + "Add to playlist": "Lisää soittolistaan", + "Add to playlist: ": "Lisää soittolistaan: ", + "Search for videos": "Etsi videoita", + "generic_button_rss": "RSS", + "Answer": "Vastaus", + "Standard YouTube license": "Vakio YouTube-lisenssi", + "Song: ": "Kappale: ", + "Album: ": "Albumi: ", + "Download is disabled": "Lataus on poistettu käytöstä", + "Channel Sponsor": "Kanavan sponsori", + "channel_tab_podcasts_label": "Podcastit", + "channel_tab_releases_label": "Julkaisut", + "channel_tab_shorts_label": "Shorts-videot", + "carousel_slide": "Dia {{current}}/{{total}}", + "carousel_skip": "Ohita karuselli", + "carousel_go_to": "Siirry diaan `x`", + "channel_tab_playlists_label": "Soittolistat", + "channel_tab_channels_label": "Kanavat", + "generic_button_delete": "Poista", + "generic_button_edit": "Muokkaa", + "generic_button_save": "Tallenna", + "generic_button_cancel": "Peru", + "playlist_button_add_items": "Lisää videoita", + "Artist: ": "Esittäjä: ", + "channel_tab_streams_label": "Suoratoistot", + "generic_channels_count": "{{count}} kanava", + "generic_channels_count_plural": "{{count}} kanavaa", + "The Popular feed has been disabled by the administrator.": "Järjestelmänvalvoja on poistanut Suositut-syötteen.", + "Import YouTube watch history (.json)": "Tuo Youtube-katseluhistoria (.json)", + "toggle_theme": "Vaihda teemaa" } From 89c17f2127fd2fc526de50da0668a72d0058685a Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 13 Aug 2024 19:51:36 +0200 Subject: [PATCH 151/155] Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Update Bulgarian translation Update German translation Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Co-authored-by: Hosted Weblate Co-authored-by: Jose Delvani Co-authored-by: Least Significant Bite Co-authored-by: NEXI Co-authored-by: Radoslav Lelchev Co-authored-by: Random Co-authored-by: Unacceptium Co-authored-by: hiatsu0 --- locales/sr.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/sr.json b/locales/sr.json index 4b24e7c0..df3177c8 100644 --- a/locales/sr.json +++ b/locales/sr.json @@ -174,7 +174,7 @@ "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Hej! Izgleda da ste isključili JavaScript. Kliknite ovde da biste videli komentare, imajte na umu da će možda potrajati malo duže da se učitaju.", "View `x` comments": { "([^.,0-9]|^)1([^.,0-9]|$)": "Pogledaj `x` komentar", - "": "Pogledaj`x` komentare" + "": "Pogledaj`x` komentara" }, "View Reddit comments": "Pogledaj Reddit komentare", "CAPTCHA is a required field": "CAPTCHA je obavezno polje", @@ -211,7 +211,7 @@ "About": "O sajtu", "footer_source_code": "Izvorni kôd", "footer_original_source_code": "Originalni izvorni kôd", - "preferences_related_videos_label": "Prikaži povezane video snimke: ", + "preferences_related_videos_label": "Prikaži srodne video snimke: ", "preferences_annotations_label": "Podrazumevano prikaži napomene: ", "preferences_extend_desc_label": "Automatski proširi opis video snimka: ", "preferences_vr_mode_label": "Interaktivni video snimci od 360 stepeni (zahteva WebGl): ", From bedcf97fbfa55280667ea9f531cb9793cd4b4fe7 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 13 Aug 2024 19:51:36 +0200 Subject: [PATCH 152/155] Update Korean translation Co-authored-by: Conflict3618 Co-authored-by: Hosted Weblate --- locales/ko.json | 50 ++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/locales/ko.json b/locales/ko.json index 7611e8e7..74395f32 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -12,14 +12,14 @@ "Dark mode: ": "다크 모드: ", "preferences_player_style_label": "플레이어 스타일: ", "preferences_category_visual": "환경 설정", - "preferences_vr_mode_label": "VR 영상 활성화(WebGL 필요): ", - "preferences_extend_desc_label": "자동으로 비디오 설명을 확장: ", + "preferences_vr_mode_label": "360도 영상 활성화 (WebGL 필요): ", + "preferences_extend_desc_label": "자동으로 비디오 설명 펼치기: ", "preferences_annotations_label": "기본으로 주석 표시: ", "preferences_related_videos_label": "관련 동영상 보기: ", "Fallback captions: ": "대체 자막: ", "preferences_captions_label": "기본 자막: ", - "reddit": "레딧", - "youtube": "유튜브", + "reddit": "Reddit", + "youtube": "YouTube", "preferences_comments_label": "기본 댓글: ", "preferences_volume_label": "플레이어 볼륨: ", "preferences_quality_label": "선호하는 비디오 품질: ", @@ -65,23 +65,23 @@ "Authorize token?": "토큰을 승인하시겠습니까?", "New passwords must match": "새 비밀번호는 일치해야 합니다", "New password": "새 비밀번호", - "Clear watch history?": "재생 기록을 삭제 하시겠습니까?", + "Clear watch history?": "시청 기록을 지우시겠습니까?", "Previous page": "이전 페이지", "Next page": "다음 페이지", "last": "마지막", "Shared `x` ago": "`x` 전", "popular": "인기", - "oldest": "오래된순", + "oldest": "과거순", "newest": "최신순", "View playlist on YouTube": "유튜브에서 재생목록 보기", "View channel on YouTube": "유튜브에서 채널 보기", "Subscribe": "구독", "Unsubscribe": "구독 취소", "LIVE": "실시간", - "generic_views_count_0": "{{count}} 조회수", - "generic_videos_count_0": "{{count}} 동영상", - "generic_playlists_count_0": "{{count}} 재생목록", - "generic_subscribers_count_0": "{{count}} 구독자", + "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}} 구독", "search_filters_type_option_playlist": "재생목록", "Korean": "한국어", @@ -109,23 +109,23 @@ "This channel does not exist.": "이 채널은 존재하지 않습니다.", "Deleted or invalid channel": "삭제되었거나 더 이상 존재하지 않는 채널", "channel:`x`": "채널:`x`", - "Show replies": "댓글 보기", + "Show replies": "댓글 보이기", "Hide replies": "댓글 숨기기", "Incorrect password": "잘못된 비밀번호", "License: ": "라이선스: ", "Genre: ": "장르: ", "Editing playlist `x`": "재생목록 `x` 수정하기", "Playlist privacy": "재생목록 공개 범위", - "Watch on YouTube": "유튜브에서 보기", + "Watch on YouTube": "YouTube에서 보기", "Show less": "간략히", "Show more": "더보기", "Title": "제목", "Create playlist": "재생목록 생성", "Trending": "급상승", "Delete playlist": "재생목록 삭제", - "Delete playlist `x`?": "재생목록 `x` 를 삭제 하시겠습니까?", + "Delete playlist `x`?": "재생목록 `x` 를 삭제하시겠습니까?", "Updated `x` ago": "`x` 전에 업데이트됨", - "Released under the AGPLv3 on Github.": "깃허브에 AGPLv3 으로 배포됩니다.", + "Released under the AGPLv3 on Github.": "GitHub에 AGPLv3 으로 배포됩니다.", "View all playlists": "모든 재생목록 보기", "Private": "비공개", "Unlisted": "목록에 없음", @@ -135,12 +135,12 @@ "Source available here.": "소스는 여기에서 사용할 수 있습니다.", "Log out": "로그아웃", "search": "검색", - "subscriptions_unseen_notifs_count_0": "{{count}} 읽지 않은 알림", + "subscriptions_unseen_notifs_count_0": "읽지 않은 알림 {{count}}개", "Subscriptions": "구독", "revoke": "철회", "unsubscribe": "구독 취소", "Import/export": "가져오기/내보내기", - "tokens_count_0": "{{count}} 토큰", + "tokens_count_0": "토큰 {{count}}개", "Token": "토큰", "Token manager": "토큰 관리자", "Subscription manager": "구독 관리자", @@ -163,7 +163,7 @@ "Clear watch history": "시청 기록 지우기", "preferences_category_data": "데이터 설정", "`x` is live": "`x` 이(가) 라이브 중입니다", - "`x` uploaded a video": "`x` 동영상 게시됨", + "`x` uploaded a video": "`x` 이(가) 동영상을 게시했습니다", "Enable web notifications": "웹 알림 활성화", "preferences_notifications_only_label": "알림만 표시 (있는 경우): ", "preferences_unseen_only_label": "시청하지 않은 것만 표시: ", @@ -241,7 +241,7 @@ "Could not create mix.": "믹스를 생성할 수 없습니다.", "`x` ago": "`x` 전", "comments_view_x_replies_0": "답글 {{count}}개 보기", - "View Reddit comments": "레딧 댓글 보기", + "View Reddit comments": "Reddit 댓글 보기", "Engagement: ": "약속: ", "Wilson score: ": "Wilson Score: ", "Family friendly? ": "전연령 영상입니까? ", @@ -267,8 +267,8 @@ "Bulgarian": "불가리아어", "Bosnian": "보스니아어", "Belarusian": "벨라루스어", - "View more comments on Reddit": "레딧에서 더 많은 댓글 보기", - "View YouTube comments": "유튜브 댓글 보기", + "View more comments on Reddit": "Reddit에서 댓글 더 보기", + "View YouTube comments": "YouTube 댓글 보기", "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "자바스크립트가 꺼져 있는 것 같습니다! 댓글을 보려면 여기를 클릭하세요. 댓글을 로드하는 데 시간이 조금 더 걸릴 수 있습니다.", "Shared `x`": "`x` 업로드", "Whitelisted regions: ": "차단되지 않은 지역: ", @@ -289,7 +289,7 @@ "Empty playlist": "재생목록 비어 있음", "Show annotations": "주석 보이기", "Hide annotations": "주석 숨기기", - "Switch Invidious Instance": "인비디어스 인스턴스 변경", + "Switch Invidious Instance": "Invidious 인스턴스 변경", "Spanish": "스페인어", "Southern Sotho": "소토어", "Somali": "소말리어", @@ -329,7 +329,7 @@ "Swedish": "스웨덴어", "Spanish (Latin America)": "스페인어 (라틴 아메리카)", "comments_points_count_0": "{{count}} 포인트", - "Invidious Private Feed for `x`": "`x` 에 대한 인비디어스 비공개 피드", + "Invidious Private Feed for `x`": "`x` 에 대한 Invidious 비공개 피드", "Premieres `x`": "최초 공개 `x`", "Premieres in `x`": "`x` 후 최초 공개", "next_steps_error_message": "다음 방법을 시도해 보세요: ", @@ -408,7 +408,7 @@ "preferences_quality_dash_option_1080p": "1080p", "preferences_quality_dash_option_worst": "최저", "preferences_watch_history_label": "시청 기록 저장: ", - "invidious": "인비디어스", + "invidious": "Invidious", "preferences_quality_option_small": "낮음", "preferences_quality_dash_option_auto": "자동", "preferences_quality_dash_option_480p": "480p", @@ -419,7 +419,7 @@ "Portuguese (Brazil)": "포르투갈어 (브라질)", "search_message_no_results": "결과가 없습니다.", "search_message_change_filters_or_query": "필터를 변경하시거나 검색어를 넓게 시도해보세요.", - "search_message_use_another_instance": " 당신은 다른 인스턴스에서 검색할 수도 있습니다.", + "search_message_use_another_instance": " 다른 인스턴스에서 검색할 수도 있습니다.", "English (United States)": "영어 (미국)", "Chinese": "중국어", "Chinese (China)": "중국어 (중국)", @@ -453,7 +453,7 @@ "channel_tab_streams_label": "실시간 스트리밍", "channel_tab_channels_label": "채널", "channel_tab_playlists_label": "재생목록", - "Standard YouTube license": "표준 유튜브 라이선스", + "Standard YouTube license": "표준 YouTube 라이선스", "Song: ": "제목: ", "Channel Sponsor": "채널 스폰서", "Album: ": "앨범: ", From a8825a27d46e32fd016a87ab3dae72168018c05e Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 13 Aug 2024 19:51:36 +0200 Subject: [PATCH 153/155] Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Update Bulgarian translation Update German translation Update Serbian (cyrillic) translation Update Serbian translation Update Finnish translation Update Italian translation Update Hungarian translation Update Portuguese (Brazil) translation Co-authored-by: Hosted Weblate Co-authored-by: Jose Delvani Co-authored-by: Least Significant Bite Co-authored-by: NEXI Co-authored-by: Radoslav Lelchev Co-authored-by: Random Co-authored-by: Unacceptium Co-authored-by: hiatsu0 --- locales/sr_Cyrl.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/sr_Cyrl.json b/locales/sr_Cyrl.json index 57c6de9c..b59fba09 100644 --- a/locales/sr_Cyrl.json +++ b/locales/sr_Cyrl.json @@ -60,7 +60,7 @@ "reddit": "Reddit", "preferences_captions_label": "Подразумевани титлови: ", "Fallback captions: ": "Резервни титлови: ", - "preferences_related_videos_label": "Прикажи повезане видео снимке: ", + "preferences_related_videos_label": "Прикажи сродне видео снимке: ", "preferences_annotations_label": "Подразумевано прикажи напомене: ", "preferences_category_visual": "Визуелна подешавања", "preferences_player_style_label": "Стил плејера: ", @@ -246,7 +246,7 @@ "preferences_locale_label": "Језик: ", "Persian": "Персијски", "View `x` comments": { - "": "Погледај `x` коментаре", + "": "Погледај `x` коментара", "([^.,0-9]|^)1([^.,0-9]|$)": "Погледај `x` коментар" }, "search_filters_type_option_channel": "Канал", From 3add83c49e11beb80510b829229bdc0b220feffe Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 13 Aug 2024 19:51:36 +0200 Subject: [PATCH 154/155] =?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: Hosted Weblate Co-authored-by: Petter Reinholdtsen --- locales/nb-NO.json | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/locales/nb-NO.json b/locales/nb-NO.json index cf0ee286..fed6d73f 100644 --- a/locales/nb-NO.json +++ b/locales/nb-NO.json @@ -21,7 +21,7 @@ "Import and Export Data": "Importer- og eksporter data", "Import": "Importer", "Import Invidious data": "Importer Invidious-JSON-data", - "Import YouTube subscriptions": "Importer YouTube/OPML-abonnementer", + "Import YouTube subscriptions": "Importer YouTube CSV eller OPML-abonnementer", "Import FreeTube subscriptions (.db)": "Importer FreeTube-abonnementer (.db)", "Import NewPipe subscriptions (.json)": "Importer NewPipe-abonnementer (.json)", "Import NewPipe data (.zip)": "Importer NewPipe-data (.zip)", @@ -487,5 +487,12 @@ "playlist_button_add_items": "Legg til videoer", "generic_channels_count": "{{count}} kanal", "generic_channels_count_plural": "{{count}} kanaler", - "Import YouTube watch history (.json)": "Importere YouTube visningshistorikk (.json)" + "Import YouTube watch history (.json)": "Importere YouTube visningshistorikk (.json)", + "carousel_go_to": "Gå til lysark `x`", + "Search for videos": "Søk i videoer", + "Answer": "Svar", + "carousel_slide": "Lysark {{current}} av {{total}}", + "carousel_skip": "Hopp over karusellen", + "Add to playlist": "Legg til i spilleliste", + "Add to playlist: ": "Legg til i spilleliste: " } From e319c35f097e08590e705378c7e5b479720deabc Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Tue, 13 Aug 2024 20:56:09 +0200 Subject: [PATCH 155/155] Videos: use intermediary variable when using CONFIG.po_token --- src/invidious/videos.cr | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index 0d26b395..6d0cf9ba 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -115,7 +115,10 @@ struct Video n = DECRYPT_FUNCTION.try &.decrypt_nsig(params["n"]) params["n"] = n if n - params["pot"] = CONFIG.po_token if CONFIG.po_token + + if token = CONFIG.po_token + params["pot"] = token + end params["host"] = url.host.not_nil! if region = self.info["region"]?.try &.as_s