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 01/54] 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 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 02/54] 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 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 03/54] 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 04/54] Update shorts params --- src/invidious/videos/parser.cr | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr index 77520dbe..75fe4a36 100644 --- a/src/invidious/videos/parser.cr +++ b/src/invidious/videos/parser.cr @@ -142,8 +142,9 @@ end def try_fetch_streaming_data(id : String, client_config : YoutubeAPI::ClientConfig) : Hash(String, JSON::Any)? LOGGER.debug("try_fetch_streaming_data: [#{id}] Using #{client_config.client_type} client.") - # 2AMBCgIQBg is a workaround for streaming URLs that returns a 403. - response = YoutubeAPI.player(video_id: id, params: "2AMBCgIQBg", client_config: client_config) + # CgIIAdgDAQ%3D%3D is a workaround for streaming URLs that returns a 403. + # https://github.com/LuanRT/YouTube.js/pull/624 + response = YoutubeAPI.player(video_id: id, params: "CgIIAdgDAQ%3D%3D", client_config: client_config) playability_status = response["playabilityStatus"]["status"] LOGGER.debug("try_fetch_streaming_data: [#{id}] Got playabilityStatus == #{playability_status}.") From 170eef58fd907057782244c95f9b8d72bb85d114 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Thu, 4 Apr 2024 19:10:27 -0400 Subject: [PATCH 05/54] 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 06/54] 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 07/54] 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 08/54] 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 09/54] 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 10/54] 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 11/54] 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 12/54] 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 13/54] 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 14/54] 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 15/54] Simplify cvm assignment logic + improve formatting Co-Authored-By: Samantaz Fox --- src/invidious/comments/youtube.cr | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/invidious/comments/youtube.cr b/src/invidious/comments/youtube.cr index 3d624325..0716fcde 100644 --- a/src/invidious/comments/youtube.cr +++ b/src/invidious/comments/youtube.cr @@ -133,15 +133,16 @@ module Invidious::Comments node_replies = node["replies"]["commentRepliesRenderer"] end - if node["commentViewModel"]? + if cvm = node["commentViewModel"]? # two commentViewModels for inital request - cvm = node.dig?("commentViewModel", "commentViewModel") # one commentViewModel when getting a replies to a comment - cvm ||= node.dig("commentViewModel") + cvm = cvm["commentViewModel"] if cvm["commentViewModel"]? + comment_key = cvm["commentKey"] toolbar_key = cvm["toolbarStateKey"] comment_mutation = mutations.find { |i| i.dig?("payload", "commentEntityPayload", "key") == comment_key } toolbar_mutation = mutations.find { |i| i.dig?("entityKey") == toolbar_key } + if !comment_mutation.nil? && !toolbar_mutation.nil? # todo parse styleRuns, commandRuns and attachmentRuns for comments html_content = parse_description(comment_mutation.dig("payload", "commentEntityPayload", "properties", "content"), id) @@ -160,17 +161,20 @@ module Invidious::Comments end end end - json.field "authorIsChannelOwner", comment_author["isCreator"].as_bool - json.field "isSponsor", (comment_author["sponsorBadgeUrl"]? != nil) - if sponsor_badge_url = comment_author["sponsorBadgeUrl"]? - # Sponsor icon thumbnails always have one object and there's only ever the url property in it - json.field "sponsorIconUrl", sponsor_badge_url - end + end + + json.field "authorIsChannelOwner", comment_author["isCreator"].as_bool + json.field "isSponsor", (comment_author["sponsorBadgeUrl"]? != nil) + + if sponsor_badge_url = comment_author["sponsorBadgeUrl"]? + # Sponsor icon thumbnails always have one object and there's only ever the url property in it + json.field "sponsorIconUrl", sponsor_badge_url end comment_toolbar = comment_mutation.dig("payload", "commentEntityPayload", "toolbar") json.field "likeCount", short_text_to_number(comment_toolbar["likeCountNotliked"].as_s) reply_count = short_text_to_number(comment_toolbar["replyCount"]?.try &.as_s || "0") + if heart_state = toolbar_mutation.dig?("payload", "engagementToolbarStateEntityPayload", "heartState") if heart_state.as_s == "TOOLBAR_HEART_STATE_HEARTED" json.field "creatorHeart" do @@ -181,8 +185,10 @@ module Invidious::Comments end end end + published_text = comment_mutation.dig?("payload", "commentEntityPayload", "properties", "publishedTime").try &.as_s end + json.field "isPinned", (cvm.dig?("pinnedText") != nil) json.field "commentId", cvm["commentId"] else From f313162fa1080bc4797dbf11ee44f51cc4c57985 Mon Sep 17 00:00:00 2001 From: absidue <48293849+absidue@users.noreply.github.com> Date: Sun, 21 Apr 2024 12:53:31 +0200 Subject: [PATCH 16/54] 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 17/54] 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 18/54] 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 19/54] 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 20/54] 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 21/54] 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 22/54] 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 23/54] 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 24/54] 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 25/54] 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 26/54] 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 27/54] 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 28/54] 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 29/54] 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 30/54] 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 31/54] 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 32/54] 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 33/54] 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 34/54] 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 35/54] 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 36/54] 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 37/54] 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 38/54] 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 39/54] 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 40/54] 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 41/54] 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 42/54] 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 43/54] 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 44/54] 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 45/54] 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 46/54] 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 47/54] 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 48/54] 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 49/54] 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 50/54] 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 51/54] 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 52/54] 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 53/54] Update changelog --- CHANGELOG.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..f6f67160 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,20 @@ +# CHANGELOG + +## 2024-04-26 + +Major bug fixes: + * Videos: Use android test suite client (#4650, thanks @SamantazFox) + * Trending: Un-nest category if this is the only one (#4600, thanks @ChunkyProgrammer) + * Comments: Add support for new format (#4576, thanks @ChunkyProgrammer) + +Minor bug fixes: + * API: Add bitrate to formatStreams too (#4590, thanks @absidue) + * API: Add 'authorVerified' field on recommended videos (#4562, thanks @ChunkyProgrammer) + * Videos: Add support for new likes format (#4462, thanks @ChunkyProgrammer) + * Proxy: Handle non-200 HTTP codes on DASH manifests (#4429, thanks @absidue) + +Other improvements: + * Remove legacy proxy code (#4570, thanks @syeopite) + * API: convey info "is post live" from Youtube response (#4569, thanks @ChunkyProgrammer) + * API: Parse channel's tags (#4294, thanks @ChunkyProgrammer) + * Translations update from Hosted Weblate (#4164, thanks to our many translators) From 1ae14cc22468ce6e0eb794752b113604b1d5582d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89milien=20=28perso=29?= <4016501+unixfox@users.noreply.github.com> Date: Mon, 27 May 2024 00:40:43 +0200 Subject: [PATCH 54/54] 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: