From c251c667487d4f2362d9527afb3c8d69cd089d0b Mon Sep 17 00:00:00 2001 From: karelrooted Date: Wed, 1 Nov 2023 11:40:06 +0800 Subject: [PATCH 01/71] fix youtube api vtt format subtitle for fmt=vtt to work the fmt parameter in the original caption api url need to be replaced --- src/invidious/routes/api/v1/videos.cr | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/invidious/routes/api/v1/videos.cr b/src/invidious/routes/api/v1/videos.cr index 1017ac9d..3bead06a 100644 --- a/src/invidious/routes/api/v1/videos.cr +++ b/src/invidious/routes/api/v1/videos.cr @@ -136,7 +136,11 @@ module Invidious::Routes::API::V1::Videos end end else - webvtt = YT_POOL.client &.get("#{url}&fmt=vtt").body + uri = URI.parse(url) + query_params = uri.query_params + query_params["fmt"] = "vtt" + uri.query_params = query_params + webvtt = YT_POOL.client &.get(uri.request_target).body if webvtt.starts_with?(" Date: Sat, 19 Aug 2023 00:25:54 +0200 Subject: [PATCH 02/71] Fix handling of modified source code URL setting --- src/invidious/routes/preferences.cr | 2 +- src/invidious/views/user/preferences.ecr | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/invidious/routes/preferences.cr b/src/invidious/routes/preferences.cr index 112535bd..05bc2714 100644 --- a/src/invidious/routes/preferences.cr +++ b/src/invidious/routes/preferences.cr @@ -214,7 +214,7 @@ module Invidious::Routes::PreferencesRoute statistics_enabled ||= "off" CONFIG.statistics_enabled = statistics_enabled == "on" - CONFIG.modified_source_code_url = env.params.body["modified_source_code_url"]?.try &.as(String) + CONFIG.modified_source_code_url = env.params.body["modified_source_code_url"]?.presence File.write("config/config.yml", CONFIG.to_yaml) end diff --git a/src/invidious/views/user/preferences.ecr b/src/invidious/views/user/preferences.ecr index 55349c5a..b89c73ca 100644 --- a/src/invidious/views/user/preferences.ecr +++ b/src/invidious/views/user/preferences.ecr @@ -310,7 +310,7 @@
- checked<% end %>> +
<% end %> From b90cf286fc947ed265031907bdb786986a399c41 Mon Sep 17 00:00:00 2001 From: absidue <48293849+absidue@users.noreply.github.com> Date: Sat, 20 Apr 2024 20:46:01 +0200 Subject: [PATCH 03/71] Fix duplicate query parameters in URLs when local=true for /api/v1/videos/{id} --- src/invidious/http_server/utils.cr | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/invidious/http_server/utils.cr b/src/invidious/http_server/utils.cr index 222dfc4a..623a9177 100644 --- a/src/invidious/http_server/utils.cr +++ b/src/invidious/http_server/utils.cr @@ -11,11 +11,12 @@ module Invidious::HttpServer params = url.query_params params["host"] = url.host.not_nil! # Should never be nil, in theory params["region"] = region if !region.nil? + url.query_params = params if absolute - return "#{HOST_URL}#{url.request_target}?#{params}" + return "#{HOST_URL}#{url.request_target}" else - return "#{url.request_target}?#{params}" + return url.request_target end end From f696f9682486b4af0aaf18eca6ced1a2a4c08dde Mon Sep 17 00:00:00 2001 From: ulmemxpoc <123284914+ulmemxpoc@users.noreply.github.com> Date: Tue, 30 Apr 2024 03:40:19 +0000 Subject: [PATCH 04/71] Add rel="noreferrer noopener" to external links --- src/invidious/frontend/comments_youtube.cr | 4 ++-- src/invidious/helpers/errors.cr | 2 +- src/invidious/views/components/video-context-buttons.ecr | 2 +- src/invidious/views/playlist.ecr | 2 +- src/invidious/views/watch.ecr | 6 +++--- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/invidious/frontend/comments_youtube.cr b/src/invidious/frontend/comments_youtube.cr index aecac87f..f9eb44ef 100644 --- a/src/invidious/frontend/comments_youtube.cr +++ b/src/invidious/frontend/comments_youtube.cr @@ -149,12 +149,12 @@ module Invidious::Frontend::Comments if comments["videoId"]? html << <<-END_HTML - [YT] + [YT] | END_HTML elsif comments["authorId"]? html << <<-END_HTML - [YT] + [YT] | END_HTML end diff --git a/src/invidious/helpers/errors.cr b/src/invidious/helpers/errors.cr index 21b789bc..b2df682d 100644 --- a/src/invidious/helpers/errors.cr +++ b/src/invidious/helpers/errors.cr @@ -190,7 +190,7 @@ def error_redirect_helper(env : HTTP::Server::Context) #{switch_instance}
  • - #{go_to_youtube} + #{go_to_youtube}
  • END_HTML diff --git a/src/invidious/views/components/video-context-buttons.ecr b/src/invidious/views/components/video-context-buttons.ecr index 385ed6b3..22458a03 100644 --- a/src/invidious/views/components/video-context-buttons.ecr +++ b/src/invidious/views/components/video-context-buttons.ecr @@ -1,6 +1,6 @@
    - " href="https://www.youtube.com/watch<%=endpoint_params%>"> + " rel="noreferrer noopener" href="https://www.youtube.com/watch<%=endpoint_params%>"> " href="/watch<%=endpoint_params%>&listen=1"> diff --git a/src/invidious/views/playlist.ecr b/src/invidious/views/playlist.ecr index 24ba437d..c27ddba6 100644 --- a/src/invidious/views/playlist.ecr +++ b/src/invidious/views/playlist.ecr @@ -83,7 +83,7 @@ <% if !playlist.is_a? InvidiousPlaylist %>
    - + <%= translate(locale, "View playlist on YouTube") %> | diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 7a1cf2c3..586b4cff 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -26,7 +26,7 @@ - + <%= rendered "components/player_sources" %> <%= title %> - Invidious @@ -123,8 +123,8 @@ we're going to need to do it here in order to allow for translations. link_yt_embed = IV::HttpServer::Utils.add_params_to_url(link_yt_embed, link_yt_param) end -%> - <%= translate(locale, "videoinfo_watch_on_youTube") %> - (<%= translate(locale, "videoinfo_youTube_embed_link") %>) + <%= translate(locale, "videoinfo_watch_on_youTube") %> + (<%= translate(locale, "videoinfo_youTube_embed_link") %>)

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