From 85a5bbd696da793e32556c50b8ae946b9358fe17 Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Thu, 26 Oct 2023 17:24:53 -0400 Subject: [PATCH 01/70] Add playlist and start time to the resolve url --- src/invidious/routes/api/v1/misc.cr | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/invidious/routes/api/v1/misc.cr b/src/invidious/routes/api/v1/misc.cr index 8a92e160c..c23fb0a5b 100644 --- a/src/invidious/routes/api/v1/misc.cr +++ b/src/invidious/routes/api/v1/misc.cr @@ -175,6 +175,8 @@ module Invidious::Routes::API::V1::Misc json.object do json.field "ucid", sub_endpoint["browseId"].as_s if sub_endpoint["browseId"]? json.field "videoId", sub_endpoint["videoId"].as_s if sub_endpoint["videoId"]? + json.field "playlistId", sub_endpoint["playlistId"].as_s if sub_endpoint["playlistId"]? + json.field "startTimeSeconds", sub_endpoint["startTimeSeconds"].as_s if sub_endpoint["startTimeSeconds"]? json.field "params", params.try &.as_s json.field "pageType", pageType end From d7901c1e0d1e9b9d36cc35d35f43c91b88ebcce4 Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Thu, 26 Oct 2023 17:35:52 -0400 Subject: [PATCH 02/70] type fix --- src/invidious/routes/api/v1/misc.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/routes/api/v1/misc.cr b/src/invidious/routes/api/v1/misc.cr index c23fb0a5b..bad47709b 100644 --- a/src/invidious/routes/api/v1/misc.cr +++ b/src/invidious/routes/api/v1/misc.cr @@ -176,7 +176,7 @@ module Invidious::Routes::API::V1::Misc json.field "ucid", sub_endpoint["browseId"].as_s if sub_endpoint["browseId"]? json.field "videoId", sub_endpoint["videoId"].as_s if sub_endpoint["videoId"]? json.field "playlistId", sub_endpoint["playlistId"].as_s if sub_endpoint["playlistId"]? - json.field "startTimeSeconds", sub_endpoint["startTimeSeconds"].as_s if sub_endpoint["startTimeSeconds"]? + json.field "startTimeSeconds", sub_endpoint["startTimeSeconds"].as_i if sub_endpoint["startTimeSeconds"]? json.field "params", params.try &.as_s json.field "pageType", pageType end From 7e267da5beef5981b6db40e7b20f23f5dbd81136 Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Thu, 26 Oct 2023 17:48:58 -0400 Subject: [PATCH 03/70] Make head request to resolve short urls --- src/invidious/routes/api/v1/misc.cr | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/invidious/routes/api/v1/misc.cr b/src/invidious/routes/api/v1/misc.cr index bad47709b..47ec977ef 100644 --- a/src/invidious/routes/api/v1/misc.cr +++ b/src/invidious/routes/api/v1/misc.cr @@ -159,6 +159,11 @@ module Invidious::Routes::API::V1::Misc return error_json(400, "Missing URL to resolve") if !url begin + head_response = HTTP::Client.head url.as(String) + if head_response.headers["location"]? + url = head_response.headers["location"] + end + resolved_url = YoutubeAPI.resolve_url(url.as(String)) endpoint = resolved_url["endpoint"] pageType = endpoint.dig?("commandMetadata", "webCommandMetadata", "webPageType").try &.as_s || "" From 3881038a32cde54bb31523adb4bbc8fd5b3d759a Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Thu, 26 Oct 2023 17:51:38 -0400 Subject: [PATCH 04/70] format --- src/invidious/routes/api/v1/misc.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/routes/api/v1/misc.cr b/src/invidious/routes/api/v1/misc.cr index 47ec977ef..c6662e324 100644 --- a/src/invidious/routes/api/v1/misc.cr +++ b/src/invidious/routes/api/v1/misc.cr @@ -163,7 +163,7 @@ module Invidious::Routes::API::V1::Misc if head_response.headers["location"]? url = head_response.headers["location"] end - + resolved_url = YoutubeAPI.resolve_url(url.as(String)) endpoint = resolved_url["endpoint"] pageType = endpoint.dig?("commandMetadata", "webCommandMetadata", "webPageType").try &.as_s || "" From b40cf6544a5e801c387b988f1be4d632fd50db90 Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Sun, 19 Nov 2023 16:06:29 -0500 Subject: [PATCH 05/70] Revert "Make head request to resolve short urls" This reverts commit 7e267da5beef5981b6db40e7b20f23f5dbd81136. --- src/invidious/routes/api/v1/misc.cr | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/invidious/routes/api/v1/misc.cr b/src/invidious/routes/api/v1/misc.cr index c6662e324..bad47709b 100644 --- a/src/invidious/routes/api/v1/misc.cr +++ b/src/invidious/routes/api/v1/misc.cr @@ -159,11 +159,6 @@ module Invidious::Routes::API::V1::Misc return error_json(400, "Missing URL to resolve") if !url begin - head_response = HTTP::Client.head url.as(String) - if head_response.headers["location"]? - url = head_response.headers["location"] - end - resolved_url = YoutubeAPI.resolve_url(url.as(String)) endpoint = resolved_url["endpoint"] pageType = endpoint.dig?("commandMetadata", "webCommandMetadata", "webPageType").try &.as_s || "" From 16c79f1ef5ab1fd58cffa8ee36c6e939efda2db4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Corn=C3=A9=20Dorrestijn?= Date: Tue, 21 Nov 2023 08:14:45 +0100 Subject: [PATCH 06/70] Fixed aspect ratio for thumnails to prevent CLS --- assets/css/default.css | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/css/default.css b/assets/css/default.css index 008812537..4d6c6c2fb 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -197,6 +197,7 @@ img.thumbnail { display: block; /* See: https://stackoverflow.com/a/11635197 */ width: 100%; object-fit: cover; + aspect-ratio: 16 / 9; } .thumbnail-placeholder { From 9310d09f9371072a2f5dba94e9dbbc6d49efa0a3 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Mon, 20 Nov 2023 17:31:21 +0100 Subject: [PATCH 07/70] Kemal: remove APIHandler middleware --- src/invidious.cr | 1 - src/invidious/helpers/handlers.cr | 68 ------------------------------- 2 files changed, 69 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index e0bd01015..c8cac80ec 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -217,7 +217,6 @@ public_folder "assets" Kemal.config.powered_by_header = false add_handler FilteredCompressHandler.new -add_handler APIHandler.new add_handler AuthHandler.new add_handler DenyFrame.new add_context_storage_type(Array(String)) diff --git a/src/invidious/helpers/handlers.cr b/src/invidious/helpers/handlers.cr index d140a8588..cece289bf 100644 --- a/src/invidious/helpers/handlers.cr +++ b/src/invidious/helpers/handlers.cr @@ -134,74 +134,6 @@ class AuthHandler < Kemal::Handler end end -class APIHandler < Kemal::Handler - {% for method in %w(GET POST PUT HEAD DELETE PATCH OPTIONS) %} - only ["/api/v1/*"], {{method}} - {% end %} - exclude ["/api/v1/auth/notifications"], "GET" - exclude ["/api/v1/auth/notifications"], "POST" - - def call(env) - return call_next env unless only_match? env - - env.response.headers["Access-Control-Allow-Origin"] = "*" - - # Since /api/v1/notifications is an event-stream, we don't want - # to wrap the response - return call_next env if exclude_match? env - - # Here we swap out the socket IO so we can modify the response as needed - output = env.response.output - env.response.output = IO::Memory.new - - begin - call_next env - - env.response.output.rewind - - if env.response.output.as(IO::Memory).size != 0 && - env.response.headers.includes_word?("Content-Type", "application/json") - response = JSON.parse(env.response.output) - - if fields_text = env.params.query["fields"]? - begin - JSONFilter.filter(response, fields_text) - rescue ex - env.response.status_code = 400 - response = {"error" => ex.message} - end - end - - if env.params.query["pretty"]?.try &.== "1" - response = response.to_pretty_json - else - response = response.to_json - end - else - response = env.response.output.gets_to_end - end - rescue ex - env.response.content_type = "application/json" if env.response.headers.includes_word?("Content-Type", "text/html") - env.response.status_code = 500 - - if env.response.headers.includes_word?("Content-Type", "application/json") - response = {"error" => ex.message || "Unspecified error"} - - if env.params.query["pretty"]?.try &.== "1" - response = response.to_pretty_json - else - response = response.to_json - end - end - ensure - env.response.output = output - env.response.print response - - env.response.flush - end - end -end - class DenyFrame < Kemal::Handler exclude ["/embed/*"] From 9d5fa2bcc4c708f15ce6e87e6da0c8922eece553 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Mon, 20 Nov 2023 17:33:26 +0100 Subject: [PATCH 08/70] Helpers: remove JSONFilter logic --- src/invidious/helpers/helpers.cr | 27 --- src/invidious/helpers/json_filter.cr | 248 --------------------------- 2 files changed, 275 deletions(-) delete mode 100644 src/invidious/helpers/json_filter.cr diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr index 6dc9860eb..6add0237f 100644 --- a/src/invidious/helpers/helpers.cr +++ b/src/invidious/helpers/helpers.cr @@ -78,15 +78,6 @@ def create_notification_stream(env, topics, connection_channel) video.published = published response = JSON.parse(video.to_json(locale, nil)) - if fields_text = env.params.query["fields"]? - begin - JSONFilter.filter(response, fields_text) - rescue ex - env.response.status_code = 400 - response = {"error" => ex.message} - end - end - env.response.puts "id: #{id}" env.response.puts "data: #{response.to_json}" env.response.puts @@ -113,15 +104,6 @@ def create_notification_stream(env, topics, connection_channel) Invidious::Database::ChannelVideos.select_notfications(topic, since_unix).each do |video| response = JSON.parse(video.to_json(locale)) - if fields_text = env.params.query["fields"]? - begin - JSONFilter.filter(response, fields_text) - rescue ex - env.response.status_code = 400 - response = {"error" => ex.message} - end - end - env.response.puts "id: #{id}" env.response.puts "data: #{response.to_json}" env.response.puts @@ -155,15 +137,6 @@ def create_notification_stream(env, topics, connection_channel) video.published = Time.unix(published) response = JSON.parse(video.to_json(locale, nil)) - if fields_text = env.params.query["fields"]? - begin - JSONFilter.filter(response, fields_text) - rescue ex - env.response.status_code = 400 - response = {"error" => ex.message} - end - end - env.response.puts "id: #{id}" env.response.puts "data: #{response.to_json}" env.response.puts diff --git a/src/invidious/helpers/json_filter.cr b/src/invidious/helpers/json_filter.cr deleted file mode 100644 index 3f4080bae..000000000 --- a/src/invidious/helpers/json_filter.cr +++ /dev/null @@ -1,248 +0,0 @@ -module JSONFilter - alias BracketIndex = Hash(Int64, Int64) - - alias GroupedFieldsValue = String | Array(GroupedFieldsValue) - alias GroupedFieldsList = Array(GroupedFieldsValue) - - class FieldsParser - class ParseError < Exception - end - - # Returns the `Regex` pattern used to match nest groups - def self.nest_group_pattern : Regex - # uses a '.' character to match json keys as they are allowed - # to contain any unicode codepoint - /(?:|,)(?[^,\n]*?)\(/ - end - - # Returns the `Regex` pattern used to check if there are any empty nest groups - def self.unnamed_nest_group_pattern : Regex - /^\(|\(\(|\/\(/ - end - - def self.parse_fields(fields_text : String, &) : Nil - if fields_text.empty? - raise FieldsParser::ParseError.new "Fields is empty" - end - - opening_bracket_count = fields_text.count('(') - closing_bracket_count = fields_text.count(')') - - if opening_bracket_count != closing_bracket_count - bracket_type = opening_bracket_count > closing_bracket_count ? "opening" : "closing" - raise FieldsParser::ParseError.new "There are too many #{bracket_type} brackets (#{opening_bracket_count}:#{closing_bracket_count})" - elsif match_result = unnamed_nest_group_pattern.match(fields_text) - raise FieldsParser::ParseError.new "Unnamed nest group at position #{match_result.begin}" - end - - # first, handle top-level single nested properties: items/id, playlistItems/snippet, etc - parse_single_nests(fields_text) { |nest_list| yield nest_list } - - # next, handle nest groups: items(id, etag, etc) - parse_nest_groups(fields_text) { |nest_list| yield nest_list } - end - - def self.parse_single_nests(fields_text : String, &) : Nil - single_nests = remove_nest_groups(fields_text) - - if !single_nests.empty? - property_nests = single_nests.split(',') - - property_nests.each do |nest| - nest_list = nest.split('/') - if nest_list.includes? "" - raise FieldsParser::ParseError.new "Empty key in nest list: #{nest_list}" - end - yield nest_list - end - # else - # raise FieldsParser::ParseError.new "Empty key in nest list 22: #{fields_text} | #{single_nests}" - end - end - - def self.parse_nest_groups(fields_text : String, &) : Nil - nest_stack = [] of NamedTuple(group_name: String, closing_bracket_index: Int64) - bracket_pairs = get_bracket_pairs(fields_text, true) - - text_index = 0 - regex_index = 0 - - while regex_result = self.nest_group_pattern.match(fields_text, regex_index) - raw_match = regex_result[0] - group_name = regex_result["groupname"] - - text_index = regex_result.begin - regex_index = regex_result.end - - if text_index.nil? || regex_index.nil? - raise FieldsParser::ParseError.new "Received invalid index while parsing nest groups: text_index: #{text_index} | regex_index: #{regex_index}" - end - - offset = raw_match.starts_with?(',') ? 1 : 0 - - opening_bracket_index = (text_index + group_name.size) + offset - closing_bracket_index = bracket_pairs[opening_bracket_index] - content_start = opening_bracket_index + 1 - - content = fields_text[content_start...closing_bracket_index] - - if content.empty? - raise FieldsParser::ParseError.new "Empty nest group at position #{content_start}" - else - content = remove_nest_groups(content) - end - - while nest_stack.size > 0 && closing_bracket_index > nest_stack[nest_stack.size - 1][:closing_bracket_index] - if nest_stack.size - nest_stack.pop - end - end - - group_name.split('/').each do |name| - nest_stack.push({ - group_name: name, - closing_bracket_index: closing_bracket_index, - }) - end - - if !content.empty? - properties = content.split(',') - - properties.each do |prop| - nest_list = nest_stack.map { |nest_prop| nest_prop[:group_name] } - - if !prop.empty? - if prop.includes?('/') - parse_single_nests(prop) { |list| nest_list += list } - else - nest_list.push prop - end - else - raise FieldsParser::ParseError.new "Empty key in nest list: #{nest_list << prop}" - end - - yield nest_list - end - end - end - end - - def self.remove_nest_groups(text : String) : String - content_bracket_pairs = get_bracket_pairs(text, false) - - content_bracket_pairs.each_key.to_a.reverse.each do |opening_bracket| - closing_bracket = content_bracket_pairs[opening_bracket] - last_comma = text.rindex(',', opening_bracket) || 0 - - text = text[0...last_comma] + text[closing_bracket + 1...text.size] - end - - return text.starts_with?(',') ? text[1...text.size] : text - end - - def self.get_bracket_pairs(text : String, recursive = true) : BracketIndex - istart = [] of Int64 - bracket_index = BracketIndex.new - - text.each_char_with_index do |char, index| - if char == '(' - istart.push(index.to_i64) - end - - if char == ')' - begin - opening = istart.pop - if recursive || (!recursive && istart.size == 0) - bracket_index[opening] = index.to_i64 - end - rescue - raise FieldsParser::ParseError.new "No matching opening parenthesis at: #{index}" - end - end - end - - if istart.size != 0 - idx = istart.pop - raise FieldsParser::ParseError.new "No matching closing parenthesis at: #{idx}" - end - - return bracket_index - end - end - - class FieldsGrouper - alias SkeletonValue = Hash(String, SkeletonValue) - - def self.create_json_skeleton(fields_text : String) : SkeletonValue - root_hash = {} of String => SkeletonValue - - FieldsParser.parse_fields(fields_text) do |nest_list| - current_item = root_hash - nest_list.each do |key| - if current_item[key]? - current_item = current_item[key] - else - current_item[key] = {} of String => SkeletonValue - current_item = current_item[key] - end - end - end - root_hash - end - - def self.create_grouped_fields_list(json_skeleton : SkeletonValue) : GroupedFieldsList - grouped_fields_list = GroupedFieldsList.new - json_skeleton.each do |key, value| - grouped_fields_list.push key - - nested_keys = create_grouped_fields_list(value) - grouped_fields_list.push nested_keys unless nested_keys.empty? - end - return grouped_fields_list - end - end - - class FilterError < Exception - end - - def self.filter(item : JSON::Any, fields_text : String, in_place : Bool = true) - skeleton = FieldsGrouper.create_json_skeleton(fields_text) - grouped_fields_list = FieldsGrouper.create_grouped_fields_list(skeleton) - filter(item, grouped_fields_list, in_place) - end - - def self.filter(item : JSON::Any, grouped_fields_list : GroupedFieldsList, in_place : Bool = true) : JSON::Any - item = item.clone unless in_place - - if !item.as_h? && !item.as_a? - raise FilterError.new "Can't filter '#{item}' by #{grouped_fields_list}" - end - - top_level_keys = Array(String).new - grouped_fields_list.each do |value| - if value.is_a? String - top_level_keys.push value - elsif value.is_a? Array - if !top_level_keys.empty? - key_to_filter = top_level_keys.last - - if item.as_h? - filter(item[key_to_filter], value, in_place: true) - elsif item.as_a? - item.as_a.each { |arr_item| filter(arr_item[key_to_filter], value, in_place: true) } - end - else - raise FilterError.new "Tried to filter while top level keys list is empty" - end - end - end - - if item.as_h? - item.as_h.select! top_level_keys - elsif item.as_a? - item.as_a.map { |value| filter(value, top_level_keys, in_place: true) } - end - - item - end -end From 7b6930c16bd731880591491490a269c73555027e Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Mon, 20 Nov 2023 17:43:57 +0100 Subject: [PATCH 09/70] Remove the 'fields' parameter on the client side too --- assets/js/notifications.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/js/notifications.js b/assets/js/notifications.js index 058553d9d..55b7a15c6 100644 --- a/assets/js/notifications.js +++ b/assets/js/notifications.js @@ -10,7 +10,7 @@ var notifications, delivered; var notifications_mock = { close: function () { } }; function get_subscriptions() { - helpers.xhr('GET', '/api/v1/auth/subscriptions?fields=authorId', { + helpers.xhr('GET', '/api/v1/auth/subscriptions', { retries: 5, entity_name: 'subscriptions' }, { @@ -22,7 +22,7 @@ function create_notification_stream(subscriptions) { // sse.js can't be replaced to EventSource in place as it lack support of payload and headers // see https://developer.mozilla.org/en-US/docs/Web/API/EventSource/EventSource notifications = new SSE( - '/api/v1/auth/notifications?fields=videoId,title,author,authorId,publishedText,published,authorThumbnails,liveNow', { + '/api/v1/auth/notifications', { withCredentials: true, payload: 'topics=' + subscriptions.map(function (subscription) { return subscription.authorId; }).join(','), headers: { 'Content-Type': 'application/x-www-form-urlencoded' } From 64887942184543c6cb5ada0909e6e1a594fdb863 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 31 Oct 2023 13:24:20 -0400 Subject: [PATCH 10/70] Unescape search suggestions --- src/invidious/routes/api/v1/search.cr | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/invidious/routes/api/v1/search.cr b/src/invidious/routes/api/v1/search.cr index 9fb283c23..a65571ead 100644 --- a/src/invidious/routes/api/v1/search.cr +++ b/src/invidious/routes/api/v1/search.cr @@ -32,11 +32,13 @@ module Invidious::Routes::API::V1::Search begin client = HTTP::Client.new("suggestqueries-clients6.youtube.com") - url = "/complete/search?client=youtube&hl=en&gl=#{region}&q=#{URI.encode_www_form(query)}&xssi=t&gs_ri=youtube&ds=yt" + client.before_request { |r| add_yt_headers(r) } + + url = "/complete/search?client=youtube&hl=en&gl=#{region}&q=#{URI.encode_www_form(query)}&gs_ri=youtube&ds=yt" response = client.get(url).body - body = JSON.parse(response[5..-1]).as_a + body = JSON.parse(response[19..-2]).as_a suggestions = body[1].as_a[0..-2] JSON.build do |json| From 8c22e6a640d458c5447fd8a841a6694f077864e4 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 14 Nov 2023 22:39:33 -0500 Subject: [PATCH 11/70] use start time and endtime for clips --- src/invidious/routes/watch.cr | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/invidious/routes/watch.cr b/src/invidious/routes/watch.cr index 3d935f0ad..1b1169d8e 100644 --- a/src/invidious/routes/watch.cr +++ b/src/invidious/routes/watch.cr @@ -275,6 +275,22 @@ module Invidious::Routes::Watch return error_template(400, "Invalid clip ID") if response["error"]? if video_id = response.dig?("endpoint", "watchEndpoint", "videoId") + if params = response.dig?("endpoint", "watchEndpoint", "params").try &.as_s + decoded_protobuf = params.try { |i| URI.decode_www_form(i) } + .try { |i| Base64.decode(i) } + .try { |i| IO::Memory.new(i) } + .try { |i| Protodec::Any.parse(i) } + + start_time = decoded_protobuf + .try(&.["50:0:embedded"]["2:1:varint"].as_i64) + + end_time = decoded_protobuf + .try(&.["50:0:embedded"]["3:2:varint"].as_i64) + + env.params.query["start"] = (start_time / 1000).to_s + env.params.query["end"] = (end_time / 1000).to_s + end + return env.redirect "/watch?v=#{video_id}&#{env.params.query}" else return error_template(404, "The requested clip doesn't exist") From b344d98c25185ca4e163eeda128b9fc68ff865b6 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 14 Nov 2023 23:35:11 -0500 Subject: [PATCH 12/70] Add API endpoint for Clips --- src/invidious/routes/api/v1/videos.cr | 43 +++++++++++++++++++++++++++ src/invidious/routes/watch.cr | 16 ++-------- src/invidious/routing.cr | 1 + src/invidious/videos/clip.cr | 20 +++++++++++++ 4 files changed, 67 insertions(+), 13 deletions(-) create mode 100644 src/invidious/videos/clip.cr diff --git a/src/invidious/routes/api/v1/videos.cr b/src/invidious/routes/api/v1/videos.cr index 1017ac9d7..9281f4ddd 100644 --- a/src/invidious/routes/api/v1/videos.cr +++ b/src/invidious/routes/api/v1/videos.cr @@ -363,4 +363,47 @@ module Invidious::Routes::API::V1::Videos end end end + + def self.clips(env) + locale = env.get("preferences").as(Preferences).locale + + env.response.content_type = "application/json" + + clip_id = env.params.url["id"] + region = env.params.query["region"]? + proxy = {"1", "true"}.any? &.== env.params.query["local"]? + + response = YoutubeAPI.resolve_url("https://www.youtube.com/clip/#{clip_id}") + return error_json(400, "Invalid clip ID") if response["error"]? + + video_id = response.dig?("endpoint", "watchEndpoint", "videoId").try &.as_s + return error_json(400, "Invalid clip ID") if video_id.nil? + + start_time = nil + end_time = nil + clip_title = nil + + if params = response.dig?("endpoint", "watchEndpoint", "params").try &.as_s + start_time, end_time, clip_title = parse_clip_parameters(params) + end + + begin + video = get_video(video_id, region: region) + rescue ex : NotFoundException + return error_json(404, ex) + rescue ex + return error_json(500, ex) + end + + return JSON.build do |json| + json.object do + json.field "startTime", start_time + json.field "endTime", end_time + json.field "clipTitle", clip_title + json.field "video" do + Invidious::JSONify::APIv1.video(video, json, locale: locale, proxy: proxy) + end + end + end + end end diff --git a/src/invidious/routes/watch.cr b/src/invidious/routes/watch.cr index 1b1169d8e..1cba86f6c 100644 --- a/src/invidious/routes/watch.cr +++ b/src/invidious/routes/watch.cr @@ -276,19 +276,9 @@ module Invidious::Routes::Watch if video_id = response.dig?("endpoint", "watchEndpoint", "videoId") if params = response.dig?("endpoint", "watchEndpoint", "params").try &.as_s - decoded_protobuf = params.try { |i| URI.decode_www_form(i) } - .try { |i| Base64.decode(i) } - .try { |i| IO::Memory.new(i) } - .try { |i| Protodec::Any.parse(i) } - - start_time = decoded_protobuf - .try(&.["50:0:embedded"]["2:1:varint"].as_i64) - - end_time = decoded_protobuf - .try(&.["50:0:embedded"]["3:2:varint"].as_i64) - - env.params.query["start"] = (start_time / 1000).to_s - env.params.query["end"] = (end_time / 1000).to_s + start_time, end_time, _ = parse_clip_parameters(params) + env.params.query["start"] = start_time.to_s + env.params.query["end"] = end_time.to_s end return env.redirect "/watch?v=#{video_id}&#{env.params.query}" diff --git a/src/invidious/routing.cr b/src/invidious/routing.cr index d6bd991c5..ba05da193 100644 --- a/src/invidious/routing.cr +++ b/src/invidious/routing.cr @@ -235,6 +235,7 @@ module Invidious::Routing get "/api/v1/captions/:id", {{namespace}}::Videos, :captions get "/api/v1/annotations/:id", {{namespace}}::Videos, :annotations get "/api/v1/comments/:id", {{namespace}}::Videos, :comments + get "/api/v1/clips/:id", {{namespace}}::Videos, :clips # Feeds get "/api/v1/trending", {{namespace}}::Feeds, :trending diff --git a/src/invidious/videos/clip.cr b/src/invidious/videos/clip.cr new file mode 100644 index 000000000..47f108a3f --- /dev/null +++ b/src/invidious/videos/clip.cr @@ -0,0 +1,20 @@ +require "json" + +# returns start_time, end_time and clip_title +def parse_clip_parameters(params) : {Float64, Float64, String} + decoded_protobuf = params.try { |i| URI.decode_www_form(i) } + .try { |i| Base64.decode(i) } + .try { |i| IO::Memory.new(i) } + .try { |i| Protodec::Any.parse(i) } + + start_time = decoded_protobuf + .try(&.["50:0:embedded"]["2:1:varint"].as_i64) + + end_time = decoded_protobuf + .try(&.["50:0:embedded"]["3:2:varint"].as_i64) + + clip_title = decoded_protobuf + .try(&.["50:0:embedded"]["4:3:string"].as_s) + + return (start_time / 1000), (end_time / 1000), clip_title +end From b5f8b4542aea3d0bb56dc682058cfdf8e666099e Mon Sep 17 00:00:00 2001 From: Chunky programmer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Wed, 7 Jun 2023 16:04:14 -0400 Subject: [PATCH 13/70] Search: Don't error if AuthorId does not exist --- src/invidious/views/components/item.ecr | 36 ++++++++++++++++++------- src/invidious/yt_backend/extractors.cr | 4 +-- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr index 031b46dad..6d227cfc9 100644 --- a/src/invidious/views/components/item.ecr +++ b/src/invidious/views/components/item.ecr @@ -82,11 +82,19 @@
- +
+ <% if !item.ucid.to_s.empty? %> + +

<%= HTML.escape(item.author) %> + <%- if author_verified %> <% end -%> +

+
+ <% else %> +

<%= HTML.escape(item.author) %> + <%- if author_verified %> <% end -%> +

+ <% end %> +
<% when Category %> <% else %> @@ -160,11 +168,19 @@
- +
+ <% if !item.ucid.to_s.empty? %> + +

<%= HTML.escape(item.author) %> + <%- if author_verified %> <% end -%> +

+
+ <% else %> +

<%= HTML.escape(item.author) %> + <%- if author_verified %> <% end -%> +

+ <% end %> +
<%= rendered "components/video-context-buttons" %>
diff --git a/src/invidious/yt_backend/extractors.cr b/src/invidious/yt_backend/extractors.cr index 56325cf7b..0e72957e6 100644 --- a/src/invidious/yt_backend/extractors.cr +++ b/src/invidious/yt_backend/extractors.cr @@ -822,9 +822,9 @@ module HelperExtractors end # Retrieves the ID required for querying the InnerTube browse endpoint. - # Raises when it's unable to do so + # Returns an empty string when it's unable to do so def self.get_browse_id(container) - return container.dig("navigationEndpoint", "browseEndpoint", "browseId").as_s + return container.dig?("navigationEndpoint", "browseEndpoint", "browseId").try &.as_s || "" end end From f1edb1d6bfbc30af46f6d3fa291d6d6fd7d66819 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 17 Oct 2023 17:49:32 -0400 Subject: [PATCH 14/70] fix related video author when id is empty --- src/invidious/views/watch.ecr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 074748969..1b020321f 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -346,7 +346,7 @@ we're going to need to do it here in order to allow for translations.
- <% if rv["ucid"]? %> + <% if !rv["ucid"].empty? %> "><%= rv["author"]? %><% if rv["author_verified"]? == "true" %> <% end %> <% else %> <%= rv["author"]? %><% if rv["author_verified"]? == "true" %> <% end %> From fe8b1b4cc4fca50de0ff5891ae5f0742d13c5d41 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Thu, 7 Dec 2023 11:43:56 -0500 Subject: [PATCH 15/70] Add title to toggle theme icon --- locales/en-US.json | 3 ++- src/invidious/views/template.ecr | 21 ++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/locales/en-US.json b/locales/en-US.json index a9f781659..227b06770 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -487,5 +487,6 @@ "channel_tab_releases_label": "Releases", "channel_tab_playlists_label": "Playlists", "channel_tab_community_label": "Community", - "channel_tab_channels_label": "Channels" + "channel_tab_channels_label": "Channels", + "toggle_theme": "Toggle Theme" } diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr index 772656795..fd755619d 100644 --- a/src/invidious/views/template.ecr +++ b/src/invidious/views/template.ecr @@ -1,5 +1,9 @@ +<% + locale = env.get("preferences").as(Preferences).locale + dark_mode = env.get("preferences").as(Preferences).dark_mode +%> -"> + @@ -20,13 +24,8 @@ -<% - locale = env.get("preferences").as(Preferences).locale - dark_mode = env.get("preferences").as(Preferences).dark_mode -%> - -theme"> - +
@@ -43,8 +42,8 @@
<% if env.get? "user" %> <% else %>
- " class="pure-menu-heading"> - <% if env.get("preferences").as(Preferences).dark_mode == "dark" %> + " class="pure-menu-heading" title="<%= translate(locale, "toggle_theme") %>"> + <% if dark_mode == "dark" %> <% else %> From 87a8207f370f20582d6330d9fcf4346fbb4e1ae5 Mon Sep 17 00:00:00 2001 From: guidiasz Date: Mon, 18 Dec 2023 13:23:55 -0300 Subject: [PATCH 16/70] fix: "Watch on YouTube" preserve current playlist --- src/invidious/views/watch.ecr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 074748969..cce6115a1 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -118,7 +118,7 @@ we're going to need to do it here in order to allow for translations. link_yt_embed = URI.new(scheme: "https", host: "www.youtube.com", path: "/embed/#{video.id}") if !plid.nil? && !continuation.nil? - link_yt_param = URI::Params{"plid" => [plid], "index" => [continuation.to_s]} + link_yt_param = URI::Params{"list" => [plid], "index" => [continuation.to_s]} link_yt_watch = IV::HttpServer::Utils.add_params_to_url(link_yt_watch, link_yt_param) link_yt_embed = IV::HttpServer::Utils.add_params_to_url(link_yt_embed, link_yt_param) end From 090b470bfcadce192439500ff89598fc6ba3faac Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 19 Dec 2023 23:07:18 -0500 Subject: [PATCH 17/70] fix potential memory leak --- src/invidious/routes/api/v1/search.cr | 1 + 1 file changed, 1 insertion(+) diff --git a/src/invidious/routes/api/v1/search.cr b/src/invidious/routes/api/v1/search.cr index a65571ead..2922b0601 100644 --- a/src/invidious/routes/api/v1/search.cr +++ b/src/invidious/routes/api/v1/search.cr @@ -37,6 +37,7 @@ module Invidious::Routes::API::V1::Search url = "/complete/search?client=youtube&hl=en&gl=#{region}&q=#{URI.encode_www_form(query)}&gs_ri=youtube&ds=yt" response = client.get(url).body + client.close body = JSON.parse(response[19..-2]).as_a suggestions = body[1].as_a[0..-2] From 0917efd9cbf4129d508217dbf38c98db5eba13cf Mon Sep 17 00:00:00 2001 From: nixos script Date: Thu, 21 Dec 2023 13:50:32 +0800 Subject: [PATCH 18/70] fix issue where scope would be missing the * if the user was not logged in before calling the authorize endpoint fix #4200 --- src/invidious/helpers/utils.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index a006d602e..e438e3b90 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -262,7 +262,7 @@ def get_referer(env, fallback = "/", unroll = true) end referer = referer.request_target - referer = "/" + referer.gsub(/[^\/?@&%=\-_.:,0-9a-zA-Z]/, "").lstrip("/\\") + referer = "/" + referer.gsub(/[^\/?@&%=\-_.:,*0-9a-zA-Z]/, "").lstrip("/\\") if referer == env.request.path referer = fallback From 7da4a7f72b0e328f72aff884605a21c4ffe7cb04 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 19 Dec 2023 22:37:48 -0500 Subject: [PATCH 19/70] add null safety to clip parsing --- src/invidious/routes/watch.cr | 4 ++-- src/invidious/videos/clip.cr | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/invidious/routes/watch.cr b/src/invidious/routes/watch.cr index 1cba86f6c..aabe8dfc2 100644 --- a/src/invidious/routes/watch.cr +++ b/src/invidious/routes/watch.cr @@ -277,8 +277,8 @@ module Invidious::Routes::Watch if video_id = response.dig?("endpoint", "watchEndpoint", "videoId") if params = response.dig?("endpoint", "watchEndpoint", "params").try &.as_s start_time, end_time, _ = parse_clip_parameters(params) - env.params.query["start"] = start_time.to_s - env.params.query["end"] = end_time.to_s + env.params.query["start"] = start_time.to_s if start_time != nil + env.params.query["end"] = end_time.to_s if end_time != nil end return env.redirect "/watch?v=#{video_id}&#{env.params.query}" diff --git a/src/invidious/videos/clip.cr b/src/invidious/videos/clip.cr index 47f108a3f..29c571825 100644 --- a/src/invidious/videos/clip.cr +++ b/src/invidious/videos/clip.cr @@ -1,7 +1,7 @@ require "json" # returns start_time, end_time and clip_title -def parse_clip_parameters(params) : {Float64, Float64, String} +def parse_clip_parameters(params) : {Float64?, Float64?, String?} decoded_protobuf = params.try { |i| URI.decode_www_form(i) } .try { |i| Base64.decode(i) } .try { |i| IO::Memory.new(i) } @@ -9,12 +9,14 @@ def parse_clip_parameters(params) : {Float64, Float64, String} start_time = decoded_protobuf .try(&.["50:0:embedded"]["2:1:varint"].as_i64) + .try { |i| i/1000 } end_time = decoded_protobuf .try(&.["50:0:embedded"]["3:2:varint"].as_i64) + .try { |i| i/1000 } clip_title = decoded_protobuf .try(&.["50:0:embedded"]["4:3:string"].as_s) - return (start_time / 1000), (end_time / 1000), clip_title + return start_time, end_time, clip_title end From c059829035855089414495c00b5212d63737b4b1 Mon Sep 17 00:00:00 2001 From: pitkajuh Date: Fri, 5 Jan 2024 20:39:29 +0100 Subject: [PATCH 20/70] Fix typo --- locales/fi.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/fi.json b/locales/fi.json index 5d8578a52..14c2b0fc6 100644 --- a/locales/fi.json +++ b/locales/fi.json @@ -14,7 +14,7 @@ "Clear watch history?": "Tyhjennä katseluhistoria?", "New password": "Uusi salasana", "New passwords must match": "Uusien salasanojen täytyy täsmätä", - "Authorize token?": "Valuutetaanko tunnus?", + "Authorize token?": "Valtuutetaanko tunnus?", "Authorize token for `x`?": "Valtuutetaanko tunnus `x`:lle?", "Yes": "Kyllä", "No": "Ei", From 7cca1285aaa1463eb31f82e49f903b437b4de69f Mon Sep 17 00:00:00 2001 From: vojkovic Date: Sat, 6 Jan 2024 15:51:31 +0800 Subject: [PATCH 21/70] Fix two swapped function names --- src/invidious/database/statistics.cr | 4 ++-- src/invidious/jobs/statistics_refresh_job.cr | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/invidious/database/statistics.cr b/src/invidious/database/statistics.cr index 1df549e25..9e4963fde 100644 --- a/src/invidious/database/statistics.cr +++ b/src/invidious/database/statistics.cr @@ -15,7 +15,7 @@ module Invidious::Database::Statistics PG_DB.query_one(request, as: Int64) end - def count_users_active_1m : Int64 + def count_users_active_6m : Int64 request = <<-SQL SELECT count(*) FROM users WHERE CURRENT_TIMESTAMP - updated < '6 months' @@ -24,7 +24,7 @@ module Invidious::Database::Statistics PG_DB.query_one(request, as: Int64) end - def count_users_active_6m : Int64 + def count_users_active_1m : Int64 request = <<-SQL SELECT count(*) FROM users WHERE CURRENT_TIMESTAMP - updated < '1 month' diff --git a/src/invidious/jobs/statistics_refresh_job.cr b/src/invidious/jobs/statistics_refresh_job.cr index 72d1ce882..66c91ad5a 100644 --- a/src/invidious/jobs/statistics_refresh_job.cr +++ b/src/invidious/jobs/statistics_refresh_job.cr @@ -56,8 +56,8 @@ class Invidious::Jobs::StatisticsRefreshJob < Invidious::Jobs::BaseJob users = STATISTICS.dig("usage", "users").as(Hash(String, Int64)) users["total"] = Invidious::Database::Statistics.count_users_total - users["activeHalfyear"] = Invidious::Database::Statistics.count_users_active_1m - users["activeMonth"] = Invidious::Database::Statistics.count_users_active_6m + users["activeHalfyear"] = Invidious::Database::Statistics.count_users_active_6m + users["activeMonth"] = Invidious::Database::Statistics.count_users_active_1m STATISTICS["metadata"] = { "updatedAt" => Time.utc.to_unix, From b16f66ef0003843c4561f7bb3124339e6b446b2b Mon Sep 17 00:00:00 2001 From: syeopite <70992037+syeopite@users.noreply.github.com> Date: Wed, 10 Jan 2024 20:40:19 +0000 Subject: [PATCH 22/70] Exempt issues with "exempt-stale" from staling (#4385) The exempt-stale label was not actually set to exempt issues from staling... --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index b25199e32..16d3269b4 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -16,7 +16,7 @@ jobs: days-before-stale: 365 days-before-pr-stale: 90 days-before-close: 30 - exempt-pr-labels: blocked + exempt-pr-labels: blocked,exempt-stale stale-issue-message: 'This issue has been automatically marked as stale and will be closed in 30 days because it has not had recent activity and is much likely outdated. If you think this issue is still relevant and applicable, you just have to post a comment and it will be unmarked.' stale-pr-message: 'This pull request has been automatically marked as stale and will be closed in 30 days because it has not had recent activity and is much likely abandoned or outdated. If you think this pull request is still relevant and applicable, you just have to post a comment and it will be unmarked.' stale-issue-label: "stale" From 1c0b4205d40781ff2d34d64dddf29e5dc89d1723 Mon Sep 17 00:00:00 2001 From: syeopite <70992037+syeopite@users.noreply.github.com> Date: Wed, 10 Jan 2024 23:01:00 +0000 Subject: [PATCH 23/70] Add parameter to disable `force_resolve` in `make_client` (#4335) * Add option to disable force_resolve in make_client Some websites such as archive.org and textcaptcha.com does not support IPv6 and as such requests fail when Invidious requests with IPv6 to those services. * Reenable force_resolve on pubsub subcribe request * Make force_resolve false by default in make_client * Remove missed explicit force_resolve=false --- src/invidious/routes/video_playback.cr | 8 ++++---- src/invidious/yt_backend/connection_pool.cr | 15 ++++++++++----- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/invidious/routes/video_playback.cr b/src/invidious/routes/video_playback.cr index 1d5aa9144..ec18f3b87 100644 --- a/src/invidious/routes/video_playback.cr +++ b/src/invidious/routes/video_playback.cr @@ -42,7 +42,7 @@ module Invidious::Routes::VideoPlayback headers["Range"] = "bytes=#{range_for_head}" end - client = make_client(URI.parse(host), region) + client = make_client(URI.parse(host), region, force_resolve = true) response = HTTP::Client::Response.new(500) error = "" 5.times do @@ -57,7 +57,7 @@ module Invidious::Routes::VideoPlayback if new_host != host host = new_host client.close - client = make_client(URI.parse(new_host), region) + client = make_client(URI.parse(new_host), region, force_resolve = true) end url = "#{location.request_target}&host=#{location.host}#{region ? "®ion=#{region}" : ""}" @@ -71,7 +71,7 @@ module Invidious::Routes::VideoPlayback fvip = "3" host = "https://r#{fvip}---#{mn}.googlevideo.com" - client = make_client(URI.parse(host), region) + client = make_client(URI.parse(host), region, force_resolve = true) rescue ex error = ex.message end @@ -196,7 +196,7 @@ module Invidious::Routes::VideoPlayback break else client.close - client = make_client(URI.parse(host), region) + client = make_client(URI.parse(host), region, force_resolve = true) end end diff --git a/src/invidious/yt_backend/connection_pool.cr b/src/invidious/yt_backend/connection_pool.cr index 36e82766d..81cfb2729 100644 --- a/src/invidious/yt_backend/connection_pool.cr +++ b/src/invidious/yt_backend/connection_pool.cr @@ -26,7 +26,7 @@ struct YoutubeConnectionPool def client(region = nil, &block) if region - conn = make_client(url, region) + conn = make_client(url, region, force_resolve = true) response = yield conn else conn = pool.checkout @@ -59,9 +59,14 @@ struct YoutubeConnectionPool end end -def make_client(url : URI, region = nil) +def make_client(url : URI, region = nil, force_resolve : Bool = false) client = HTTPClient.new(url, OpenSSL::SSL::Context::Client.insecure) - client.family = CONFIG.force_resolve + + # Some services do not support IPv6. + if force_resolve + client.family = CONFIG.force_resolve + end + client.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com" client.read_timeout = 10.seconds client.connect_timeout = 10.seconds @@ -80,8 +85,8 @@ def make_client(url : URI, region = nil) return client end -def make_client(url : URI, region = nil, &block) - client = make_client(url, region) +def make_client(url : URI, region = nil, force_resolve : Bool = false, &block) + client = make_client(url, region, force_resolve) begin yield client ensure From 4a339df5c49e30a5ef5008d26639eb69edfff152 Mon Sep 17 00:00:00 2001 From: toabr <25079664+toabr@users.noreply.github.com> Date: Sat, 27 Jan 2024 00:38:47 +0100 Subject: [PATCH 24/70] CSS: expand #contents width on small screens --- assets/css/default.css | 1 + src/invidious/views/template.ecr | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/assets/css/default.css b/assets/css/default.css index 008812537..fd696178f 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -13,6 +13,7 @@ body { display: flex; flex-direction: column; min-height: 100vh; + margin: auto; } .h-box { diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr index 772656795..5e2cf88e4 100644 --- a/src/invidious/views/template.ecr +++ b/src/invidious/views/template.ecr @@ -28,8 +28,7 @@ -theme">
-
-
+
From c005ada48723808e507d0a4d5a3363a1c14a4f07 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Mon, 29 Jan 2024 14:59:25 +0100 Subject: [PATCH 25/70] fix: prevent censoring of self-harm related search queries (#4403) * fix: prevent censoring of self-harm related search queries * fix: yt_filters_spec with new flag --- spec/invidious/search/yt_filters_spec.cr | 54 ++++++++++++------------ src/invidious/search/filters.cr | 6 +-- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/spec/invidious/search/yt_filters_spec.cr b/spec/invidious/search/yt_filters_spec.cr index bf7f21e7a..8abed5ce5 100644 --- a/spec/invidious/search/yt_filters_spec.cr +++ b/spec/invidious/search/yt_filters_spec.cr @@ -12,45 +12,45 @@ end # page of Youtube with any browser devtools HTML inspector. DATE_FILTERS = { - Invidious::Search::Filters::Date::Hour => "EgIIAQ%3D%3D", - Invidious::Search::Filters::Date::Today => "EgIIAg%3D%3D", - Invidious::Search::Filters::Date::Week => "EgIIAw%3D%3D", - Invidious::Search::Filters::Date::Month => "EgIIBA%3D%3D", - Invidious::Search::Filters::Date::Year => "EgIIBQ%3D%3D", + Invidious::Search::Filters::Date::Hour => "EgIIAfABAQ%3D%3D", + Invidious::Search::Filters::Date::Today => "EgIIAvABAQ%3D%3D", + Invidious::Search::Filters::Date::Week => "EgIIA_ABAQ%3D%3D", + Invidious::Search::Filters::Date::Month => "EgIIBPABAQ%3D%3D", + Invidious::Search::Filters::Date::Year => "EgIIBfABAQ%3D%3D", } TYPE_FILTERS = { - Invidious::Search::Filters::Type::Video => "EgIQAQ%3D%3D", - Invidious::Search::Filters::Type::Channel => "EgIQAg%3D%3D", - Invidious::Search::Filters::Type::Playlist => "EgIQAw%3D%3D", - Invidious::Search::Filters::Type::Movie => "EgIQBA%3D%3D", + Invidious::Search::Filters::Type::Video => "EgIQAfABAQ%3D%3D", + Invidious::Search::Filters::Type::Channel => "EgIQAvABAQ%3D%3D", + Invidious::Search::Filters::Type::Playlist => "EgIQA_ABAQ%3D%3D", + Invidious::Search::Filters::Type::Movie => "EgIQBPABAQ%3D%3D", } DURATION_FILTERS = { - Invidious::Search::Filters::Duration::Short => "EgIYAQ%3D%3D", - Invidious::Search::Filters::Duration::Medium => "EgIYAw%3D%3D", - Invidious::Search::Filters::Duration::Long => "EgIYAg%3D%3D", + Invidious::Search::Filters::Duration::Short => "EgIYAfABAQ%3D%3D", + Invidious::Search::Filters::Duration::Medium => "EgIYA_ABAQ%3D%3D", + Invidious::Search::Filters::Duration::Long => "EgIYAvABAQ%3D%3D", } FEATURE_FILTERS = { - Invidious::Search::Filters::Features::Live => "EgJAAQ%3D%3D", - Invidious::Search::Filters::Features::FourK => "EgJwAQ%3D%3D", - Invidious::Search::Filters::Features::HD => "EgIgAQ%3D%3D", - Invidious::Search::Filters::Features::Subtitles => "EgIoAQ%3D%3D", - Invidious::Search::Filters::Features::CCommons => "EgIwAQ%3D%3D", - Invidious::Search::Filters::Features::ThreeSixty => "EgJ4AQ%3D%3D", - Invidious::Search::Filters::Features::VR180 => "EgPQAQE%3D", - Invidious::Search::Filters::Features::ThreeD => "EgI4AQ%3D%3D", - Invidious::Search::Filters::Features::HDR => "EgPIAQE%3D", - Invidious::Search::Filters::Features::Location => "EgO4AQE%3D", - Invidious::Search::Filters::Features::Purchased => "EgJIAQ%3D%3D", + Invidious::Search::Filters::Features::Live => "EgJAAfABAQ%3D%3D", + Invidious::Search::Filters::Features::FourK => "EgJwAfABAQ%3D%3D", + Invidious::Search::Filters::Features::HD => "EgIgAfABAQ%3D%3D", + Invidious::Search::Filters::Features::Subtitles => "EgIoAfABAQ%3D%3D", + Invidious::Search::Filters::Features::CCommons => "EgIwAfABAQ%3D%3D", + Invidious::Search::Filters::Features::ThreeSixty => "EgJ4AfABAQ%3D%3D", + Invidious::Search::Filters::Features::VR180 => "EgPQAQHwAQE%3D", + Invidious::Search::Filters::Features::ThreeD => "EgI4AfABAQ%3D%3D", + Invidious::Search::Filters::Features::HDR => "EgPIAQHwAQE%3D", + Invidious::Search::Filters::Features::Location => "EgO4AQHwAQE%3D", + Invidious::Search::Filters::Features::Purchased => "EgJIAfABAQ%3D%3D", } SORT_FILTERS = { - Invidious::Search::Filters::Sort::Relevance => "", - Invidious::Search::Filters::Sort::Date => "CAI%3D", - Invidious::Search::Filters::Sort::Views => "CAM%3D", - Invidious::Search::Filters::Sort::Rating => "CAE%3D", + Invidious::Search::Filters::Sort::Relevance => "8AEB", + Invidious::Search::Filters::Sort::Date => "CALwAQE%3D", + Invidious::Search::Filters::Sort::Views => "CAPwAQE%3D", + Invidious::Search::Filters::Sort::Rating => "CAHwAQE%3D", } Spectator.describe Invidious::Search::Filters do diff --git a/src/invidious/search/filters.cr b/src/invidious/search/filters.cr index c2b5c7581..bf968734c 100644 --- a/src/invidious/search/filters.cr +++ b/src/invidious/search/filters.cr @@ -300,9 +300,9 @@ module Invidious::Search object["9:varint"] = ((page - 1) * 20).to_i64 end - # If the object is empty, return an empty string, - # otherwise encode to protobuf then to base64 - return "" if object.empty? + # Prevent censoring of self harm topics + # See https://github.com/iv-org/invidious/issues/4398 + object["30:varint"] = 1.to_i64 return object .try { |i| Protodec::Any.cast_json(i) } From 0ad2eff2a46c28a877de1960a2dc5c15c0f94444 Mon Sep 17 00:00:00 2001 From: syeopite Date: Tue, 30 Jan 2024 15:25:45 -0800 Subject: [PATCH 26/70] WebVTT::Builder: Add logic to escape special chars --- spec/helpers/vtt/builder_spec.cr | 65 +++++++++++++++++++++----------- src/invidious/helpers/webvtt.cr | 16 +++++++- 2 files changed, 59 insertions(+), 22 deletions(-) diff --git a/spec/helpers/vtt/builder_spec.cr b/spec/helpers/vtt/builder_spec.cr index 7b543ddcb..dc1f4613c 100644 --- a/spec/helpers/vtt/builder_spec.cr +++ b/spec/helpers/vtt/builder_spec.cr @@ -1,34 +1,27 @@ require "../../spec_helper.cr" -MockLines = [ - { - "start_time": Time::Span.new(seconds: 1), - "end_time": Time::Span.new(seconds: 2), - "text": "Line 1", - }, - - { - "start_time": Time::Span.new(seconds: 2), - "end_time": Time::Span.new(seconds: 3), - "text": "Line 2", - }, -] +MockLines = ["Line 1", "Line 2"] +MockLinesWithEscapableCharacter = ["", "&Line 2>", '\u200E' + "Line\u200F 3", "\u00A0Line 4"] Spectator.describe "WebVTT::Builder" do it "correctly builds a vtt file" do result = WebVTT.build do |vtt| - MockLines.each do |line| - vtt.cue(line["start_time"], line["end_time"], line["text"]) + 2.times do |i| + vtt.cue( + Time::Span.new(seconds: i), + Time::Span.new(seconds: i + 1), + MockLines[i] + ) end end expect(result).to eq([ "WEBVTT", "", - "00:00:01.000 --> 00:00:02.000", + "00:00:00.000 --> 00:00:01.000", "Line 1", "", - "00:00:02.000 --> 00:00:03.000", + "00:00:01.000 --> 00:00:02.000", "Line 2", "", "", @@ -42,8 +35,12 @@ Spectator.describe "WebVTT::Builder" do } result = WebVTT.build(setting_fields) do |vtt| - MockLines.each do |line| - vtt.cue(line["start_time"], line["end_time"], line["text"]) + 2.times do |i| + vtt.cue( + Time::Span.new(seconds: i), + Time::Span.new(seconds: i + 1), + MockLines[i] + ) end end @@ -52,13 +49,39 @@ Spectator.describe "WebVTT::Builder" do "Kind: captions", "Language: en", "", - "00:00:01.000 --> 00:00:02.000", + "00:00:00.000 --> 00:00:01.000", "Line 1", "", - "00:00:02.000 --> 00:00:03.000", + "00:00:01.000 --> 00:00:02.000", "Line 2", "", "", ].join('\n')) end + + it "properly escapes characters" do + result = WebVTT.build do |vtt| + 4.times do |i| + vtt.cue(Time::Span.new(seconds: i), Time::Span.new(seconds: i + 1), MockLinesWithEscapableCharacter[i]) + end + end + + expect(result).to eq([ + "WEBVTT", + "", + "00:00:00.000 --> 00:00:01.000", + "<Line 1>", + "", + "00:00:01.000 --> 00:00:02.000", + "&Line 2>", + "", + "00:00:02.000 --> 00:00:03.000", + "‎Line‏ 3", + "", + "00:00:03.000 --> 00:00:04.000", + " Line 4", + "", + "", + ].join('\n')) + end end diff --git a/src/invidious/helpers/webvtt.cr b/src/invidious/helpers/webvtt.cr index 56f761edb..260d250fa 100644 --- a/src/invidious/helpers/webvtt.cr +++ b/src/invidious/helpers/webvtt.cr @@ -4,13 +4,23 @@ module WebVTT # A WebVTT builder generates WebVTT files private class Builder + # See https://developer.mozilla.org/en-US/docs/Web/API/WebVTT_API#cue_payload + private ESCAPE_SUBSTITUTIONS = { + '&' => "&", + '<' => "<", + '>' => ">", + '\u200E' => "‎", + '\u200F' => "‏", + '\u00A0' => " ", + } + def initialize(@io : IO) end # Writes an vtt cue with the specified time stamp and contents def cue(start_time : Time::Span, end_time : Time::Span, text : String) timestamp(start_time, end_time) - @io << text + @io << self.escape(text) @io << "\n\n" end @@ -29,6 +39,10 @@ module WebVTT @io << '.' << timestamp.milliseconds.to_s.rjust(3, '0') end + private def escape(text : String) : String + return text.gsub(ESCAPE_SUBSTITUTIONS) + end + def document(setting_fields : Hash(String, String)? = nil, &) @io << "WEBVTT\n" From c864a63b6d86cb7552d2f1730731e427fc435fe4 Mon Sep 17 00:00:00 2001 From: shironeko Date: Thu, 8 Feb 2024 17:05:11 -0500 Subject: [PATCH 27/70] Fix pubsub feed parsing similar to what's done in #3793, this is causing an assert on my instance --- src/invidious/routes/feeds.cr | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/invidious/routes/feeds.cr b/src/invidious/routes/feeds.cr index 40bca0088..512d4ee72 100644 --- a/src/invidious/routes/feeds.cr +++ b/src/invidious/routes/feeds.cr @@ -407,12 +407,17 @@ module Invidious::Routes::Feeds end spawn do - rss = XML.parse_html(body) - rss.xpath_nodes("//feed/entry").each do |entry| - id = entry.xpath_node("videoid").not_nil!.content - author = entry.xpath_node("author/name").not_nil!.content - published = Time.parse_rfc3339(entry.xpath_node("published").not_nil!.content) - updated = Time.parse_rfc3339(entry.xpath_node("updated").not_nil!.content) + # TODO: unify this with the other almost identical looking parts in this and channels.cr somehow? + namespaces = { + "yt" => "http://www.youtube.com/xml/schemas/2015", + "default" => "http://www.w3.org/2005/Atom", + } + rss = XML.parse(body) + rss.xpath_nodes("//default:feed/default:entry", namespaces).each do |entry| + id = entry.xpath_node("yt:videoId", namespaces).not_nil!.content + author = entry.xpath_node("default:author/default:name", namespaces).not_nil!.content + published = Time.parse_rfc3339(entry.xpath_node("default:published", namespaces).not_nil!.content) + updated = Time.parse_rfc3339(entry.xpath_node("default:updated", namespaces).not_nil!.content) video = get_video(id, force_refresh: true) From 98c421e9f539f8d72e6842fea94d17ff0db7f38a Mon Sep 17 00:00:00 2001 From: shironeko Date: Thu, 8 Feb 2024 18:58:23 -0500 Subject: [PATCH 28/70] Fix when video from pubsub is a scheduled event --- src/invidious/routes/feeds.cr | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/invidious/routes/feeds.cr b/src/invidious/routes/feeds.cr index 512d4ee72..e20a7139e 100644 --- a/src/invidious/routes/feeds.cr +++ b/src/invidious/routes/feeds.cr @@ -419,7 +419,11 @@ module Invidious::Routes::Feeds published = Time.parse_rfc3339(entry.xpath_node("default:published", namespaces).not_nil!.content) updated = Time.parse_rfc3339(entry.xpath_node("default:updated", namespaces).not_nil!.content) - video = get_video(id, force_refresh: true) + begin + video = get_video(id, force_refresh: true) + rescue + next # skip this video since it raised an exception (e.g. it is a scheduled live event) + end if CONFIG.enable_user_notifications # Deliver notifications to `/api/v1/auth/notifications` From 6b33820f1f13171c3b432d6bc548b23380a3790d Mon Sep 17 00:00:00 2001 From: shironeko Date: Thu, 8 Feb 2024 18:23:08 -0500 Subject: [PATCH 29/70] Add missing translation strings closes #3120 --- locales/en-US.json | 5 +++++ src/invidious/frontend/comments_reddit.cr | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/locales/en-US.json b/locales/en-US.json index a9f781659..29fc7db64 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -1,4 +1,9 @@ { + "Add to playlist": "Add to playlist", + "Add to playlist: ": "Add to playlist: ", + "Answer": "Answer", + "Search for videos": "Search for videos", + "The Popular feed has been disabled by the administrator.": "The Popular feed has been disabled by the administrator.", "generic_channels_count": "{{count}} channel", "generic_channels_count_plural": "{{count}} channels", "generic_views_count": "{{count}} view", diff --git a/src/invidious/frontend/comments_reddit.cr b/src/invidious/frontend/comments_reddit.cr index b5647bae8..4dda683ef 100644 --- a/src/invidious/frontend/comments_reddit.cr +++ b/src/invidious/frontend/comments_reddit.cr @@ -33,7 +33,7 @@ module Invidious::Frontend::Comments [ − ] #{child.author} #{translate_count(locale, "comments_points_count", child.score, NumberFormatting::Separator)} - #{translate(locale, "`x` ago", recode_date(child.created_utc, locale))} + #{translate(locale, "`x` ago", recode_date(child.created_utc, locale))} #{translate(locale, "permalink")}

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