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

    From 9d66676f2dbb18a87ca7515e839f1c64688ecd39 Mon Sep 17 00:00:00 2001 From: Fijxu Date: Wed, 1 May 2024 22:17:41 -0400 Subject: [PATCH 019/136] Use full URL in the og:image property. --- src/invidious/views/channel.ecr | 4 ++-- src/invidious/views/watch.ecr | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/invidious/views/channel.ecr b/src/invidious/views/channel.ecr index 09df106d..a84e44bc 100644 --- a/src/invidious/views/channel.ecr +++ b/src/invidious/views/channel.ecr @@ -30,13 +30,13 @@ - + - + <%- end -%> diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 7a1cf2c3..9e7467dd 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -10,7 +10,7 @@ - + From c4fec89a9bac0228f6fac6ab2e8547132b57cc98 Mon Sep 17 00:00:00 2001 From: ulmemxpoc <123284914+ulmemxpoc@users.noreply.github.com> Date: Fri, 10 May 2024 11:23:11 -0700 Subject: [PATCH 020/136] 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 021/136] 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 022/136] 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 023/136] 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 024/136] 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 025/136] 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 026/136] 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 9cd2e93a2ee8f2f0f570bcb8fbe584f5c502a34e Mon Sep 17 00:00:00 2001 From: thansk <53181514+thansk@users.noreply.github.com> Date: Sun, 19 May 2024 11:46:55 +0000 Subject: [PATCH 027/136] feat: allow submitting search with mouse --- assets/css/default.css | 19 ++++++++++++++++++- src/invidious/views/components/search_box.ecr | 1 + 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/assets/css/default.css b/assets/css/default.css index a47762ec..d86ec7bc 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -278,7 +278,14 @@ div.thumbnail > .bottom-right-overlay { display: inline; } -.searchbar .pure-form fieldset { padding: 0; } +.searchbar .pure-form { + display: flex; +} + +.searchbar .pure-form fieldset { + padding: 0; + flex: 1; +} .searchbar input[type="search"] { width: 100%; @@ -310,6 +317,16 @@ input[type="search"]::-webkit-search-cancel-button { background-size: 14px; } +.searchbar #searchbutton { + border: 0; + background: none; + text-transform: uppercase; +} + +.searchbar #searchbutton:hover { + color: rgb(0, 182, 240); +} + .user-field { display: flex; flex-direction: row; diff --git a/src/invidious/views/components/search_box.ecr b/src/invidious/views/components/search_box.ecr index a03785d1..c5488255 100644 --- a/src/invidious/views/components/search_box.ecr +++ b/src/invidious/views/components/search_box.ecr @@ -6,4 +6,5 @@ title="<%= translate(locale, "search") %>" value="<%= env.get?("search").try {|x| HTML.escape(x.as(String)) } %>"> + From 5abafb8296330dfc7fe7ab630661e0cc8e04ef85 Mon Sep 17 00:00:00 2001 From: thansk <53181514+thansk@users.noreply.github.com> Date: Mon, 20 May 2024 11:49:56 +0000 Subject: [PATCH 028/136] fix: use a search icon instead of text --- assets/css/default.css | 3 +++ src/invidious/views/components/search_box.ecr | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/assets/css/default.css b/assets/css/default.css index d86ec7bc..20ec3222 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -321,6 +321,9 @@ input[type="search"]::-webkit-search-cancel-button { border: 0; background: none; text-transform: uppercase; + display: grid; + place-items: center; + width: 1.5em; } .searchbar #searchbutton:hover { diff --git a/src/invidious/views/components/search_box.ecr b/src/invidious/views/components/search_box.ecr index c5488255..b679b031 100644 --- a/src/invidious/views/components/search_box.ecr +++ b/src/invidious/views/components/search_box.ecr @@ -6,5 +6,9 @@ title="<%= translate(locale, "search") %>" value="<%= env.get?("search").try {|x| HTML.escape(x.as(String)) } %>"> - + From 1ce2d10c505a7e0c3972acfb626a0ae3c9af3d57 Mon Sep 17 00:00:00 2001 From: thansk <53181514+thansk@users.noreply.github.com> Date: Mon, 20 May 2024 14:17:30 +0000 Subject: [PATCH 029/136] fix: use ion icon for search icon --- assets/css/default.css | 7 ++----- src/invidious/views/components/search_box.ecr | 4 +--- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/assets/css/default.css b/assets/css/default.css index 20ec3222..1445f65f 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -318,12 +318,9 @@ input[type="search"]::-webkit-search-cancel-button { } .searchbar #searchbutton { - border: 0; + border: none; background: none; - text-transform: uppercase; - display: grid; - place-items: center; - width: 1.5em; + margin-top: 0; } .searchbar #searchbutton:hover { diff --git a/src/invidious/views/components/search_box.ecr b/src/invidious/views/components/search_box.ecr index b679b031..29da2c52 100644 --- a/src/invidious/views/components/search_box.ecr +++ b/src/invidious/views/components/search_box.ecr @@ -7,8 +7,6 @@ value="<%= env.get?("search").try {|x| HTML.escape(x.as(String)) } %>"> From 1ae14cc22468ce6e0eb794752b113604b1d5582d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89milien=20=28perso=29?= <4016501+unixfox@users.noreply.github.com> Date: Mon, 27 May 2024 00:40:43 +0200 Subject: [PATCH 030/136] move helm chart to a dedicated github repository (#4711) --- kubernetes/.gitignore | 1 - kubernetes/Chart.lock | 6 --- kubernetes/Chart.yaml | 22 ---------- kubernetes/README.md | 42 +------------------ kubernetes/templates/_helpers.tpl | 16 -------- kubernetes/templates/configmap.yaml | 11 ----- kubernetes/templates/deployment.yaml | 61 ---------------------------- kubernetes/templates/hpa.yaml | 18 -------- kubernetes/templates/service.yaml | 20 --------- kubernetes/values.yaml | 61 ---------------------------- 10 files changed, 1 insertion(+), 257 deletions(-) delete mode 100644 kubernetes/.gitignore delete mode 100644 kubernetes/Chart.lock delete mode 100644 kubernetes/Chart.yaml delete mode 100644 kubernetes/templates/_helpers.tpl delete mode 100644 kubernetes/templates/configmap.yaml delete mode 100644 kubernetes/templates/deployment.yaml delete mode 100644 kubernetes/templates/hpa.yaml delete mode 100644 kubernetes/templates/service.yaml delete mode 100644 kubernetes/values.yaml diff --git a/kubernetes/.gitignore b/kubernetes/.gitignore deleted file mode 100644 index 0ad51707..00000000 --- a/kubernetes/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/charts/*.tgz diff --git a/kubernetes/Chart.lock b/kubernetes/Chart.lock deleted file mode 100644 index ef12b0b6..00000000 --- a/kubernetes/Chart.lock +++ /dev/null @@ -1,6 +0,0 @@ -dependencies: -- name: postgresql - repository: https://charts.bitnami.com/bitnami/ - version: 12.11.1 -digest: sha256:3c10008175c4f5c1cec38782f5a7316154b89074c77ebbd9bcc4be4f5ff21122 -generated: "2023-09-14T22:40:43.171275362Z" diff --git a/kubernetes/Chart.yaml b/kubernetes/Chart.yaml deleted file mode 100644 index d22f6254..00000000 --- a/kubernetes/Chart.yaml +++ /dev/null @@ -1,22 +0,0 @@ -apiVersion: v2 -name: invidious -description: Invidious is an alternative front-end to YouTube -version: 1.1.1 -appVersion: 0.20.1 -keywords: -- youtube -- proxy -- video -- privacy -home: https://invidio.us/ -icon: https://raw.githubusercontent.com/iv-org/invidious/05988c1c49851b7d0094fca16aeaf6382a7f64ab/assets/favicon-32x32.png -sources: -- https://github.com/iv-org/invidious -maintainers: -- name: Leon Klingele - email: mail@leonklingele.de -dependencies: -- name: postgresql - version: ~12.11.0 - repository: "https://charts.bitnami.com/bitnami/" -engine: gotpl diff --git a/kubernetes/README.md b/kubernetes/README.md index 35478f99..e71f6a86 100644 --- a/kubernetes/README.md +++ b/kubernetes/README.md @@ -1,41 +1 @@ -# Invidious Helm chart - -Easily deploy Invidious to Kubernetes. - -## Installing Helm chart - -```sh -# Build Helm dependencies -$ helm dep build - -# Add PostgreSQL init scripts -$ kubectl create configmap invidious-postgresql-init \ - --from-file=../config/sql/channels.sql \ - --from-file=../config/sql/videos.sql \ - --from-file=../config/sql/channel_videos.sql \ - --from-file=../config/sql/users.sql \ - --from-file=../config/sql/session_ids.sql \ - --from-file=../config/sql/nonces.sql \ - --from-file=../config/sql/annotations.sql \ - --from-file=../config/sql/playlists.sql \ - --from-file=../config/sql/playlist_videos.sql - -# Install Helm app to your Kubernetes cluster -$ helm install invidious ./ -``` - -## Upgrading - -```sh -# Upgrading is easy, too! -$ helm upgrade invidious ./ -``` - -## Uninstall - -```sh -# Get rid of everything (except database) -$ helm delete invidious - -# To also delete the database, remove all invidious-postgresql PVCs -``` +The Helm chart has moved to a dedicated GitHub repository: https://github.com/iv-org/invidious-helm-chart/tree/master/invidious \ No newline at end of file diff --git a/kubernetes/templates/_helpers.tpl b/kubernetes/templates/_helpers.tpl deleted file mode 100644 index 52158b78..00000000 --- a/kubernetes/templates/_helpers.tpl +++ /dev/null @@ -1,16 +0,0 @@ -{{/* vim: set filetype=mustache: */}} -{{/* -Expand the name of the chart. -*/}} -{{- define "invidious.name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} -{{- end -}} - -{{/* -Create a default fully qualified app name. -We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). -*/}} -{{- define "invidious.fullname" -}} -{{- $name := default .Chart.Name .Values.nameOverride -}} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} -{{- end -}} diff --git a/kubernetes/templates/configmap.yaml b/kubernetes/templates/configmap.yaml deleted file mode 100644 index 58542a31..00000000 --- a/kubernetes/templates/configmap.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ template "invidious.fullname" . }} - labels: - app: {{ template "invidious.name" . }} - chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" - release: {{ .Release.Name }} -data: - INVIDIOUS_CONFIG: | -{{ toYaml .Values.config | indent 4 }} diff --git a/kubernetes/templates/deployment.yaml b/kubernetes/templates/deployment.yaml deleted file mode 100644 index bb0b832f..00000000 --- a/kubernetes/templates/deployment.yaml +++ /dev/null @@ -1,61 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ template "invidious.fullname" . }} - labels: - app: {{ template "invidious.name" . }} - chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" - release: {{ .Release.Name }} -spec: - replicas: {{ .Values.replicaCount }} - selector: - matchLabels: - app: {{ template "invidious.name" . }} - release: {{ .Release.Name }} - template: - metadata: - labels: - app: {{ template "invidious.name" . }} - chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" - release: {{ .Release.Name }} - spec: - securityContext: - runAsUser: {{ .Values.securityContext.runAsUser }} - runAsGroup: {{ .Values.securityContext.runAsGroup }} - fsGroup: {{ .Values.securityContext.fsGroup }} - initContainers: - - name: wait-for-postgresql - image: postgres - args: - - /bin/sh - - -c - - until pg_isready -h {{ .Values.config.db.host }} -p {{ .Values.config.db.port }} -U {{ .Values.config.db.user }}; do echo waiting for database; sleep 2; done; - containers: - - name: {{ .Chart.Name }} - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" - imagePullPolicy: {{ .Values.image.pullPolicy }} - ports: - - containerPort: 3000 - env: - - name: INVIDIOUS_CONFIG - valueFrom: - configMapKeyRef: - key: INVIDIOUS_CONFIG - name: {{ template "invidious.fullname" . }} - securityContext: - allowPrivilegeEscalation: {{ .Values.securityContext.allowPrivilegeEscalation }} - capabilities: - drop: - - ALL - resources: -{{ toYaml .Values.resources | indent 10 }} - readinessProbe: - httpGet: - port: 3000 - path: / - livenessProbe: - httpGet: - port: 3000 - path: / - initialDelaySeconds: 15 - restartPolicy: Always diff --git a/kubernetes/templates/hpa.yaml b/kubernetes/templates/hpa.yaml deleted file mode 100644 index c6fbefe2..00000000 --- a/kubernetes/templates/hpa.yaml +++ /dev/null @@ -1,18 +0,0 @@ -{{- if .Values.autoscaling.enabled }} -apiVersion: autoscaling/v1 -kind: HorizontalPodAutoscaler -metadata: - name: {{ template "invidious.fullname" . }} - labels: - app: {{ template "invidious.name" . }} - chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" - release: {{ .Release.Name }} -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: {{ template "invidious.fullname" . }} - minReplicas: {{ .Values.autoscaling.minReplicas }} - maxReplicas: {{ .Values.autoscaling.maxReplicas }} - targetCPUUtilizationPercentage: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} -{{- end }} diff --git a/kubernetes/templates/service.yaml b/kubernetes/templates/service.yaml deleted file mode 100644 index 01454d4e..00000000 --- a/kubernetes/templates/service.yaml +++ /dev/null @@ -1,20 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: {{ template "invidious.fullname" . }} - labels: - app: {{ template "invidious.name" . }} - chart: {{ .Chart.Name }} - release: {{ .Release.Name }} -spec: - type: {{ .Values.service.type }} - ports: - - name: http - port: {{ .Values.service.port }} - targetPort: 3000 - selector: - app: {{ template "invidious.name" . }} - release: {{ .Release.Name }} -{{- if .Values.service.loadBalancerIP }} - loadBalancerIP: {{ .Values.service.loadBalancerIP }} -{{- end }} diff --git a/kubernetes/values.yaml b/kubernetes/values.yaml deleted file mode 100644 index 5000c2b6..00000000 --- a/kubernetes/values.yaml +++ /dev/null @@ -1,61 +0,0 @@ -name: invidious - -image: - repository: quay.io/invidious/invidious - tag: latest - pullPolicy: Always - -replicaCount: 1 - -autoscaling: - enabled: false - minReplicas: 1 - maxReplicas: 16 - targetCPUUtilizationPercentage: 50 - -service: - type: ClusterIP - port: 3000 - #loadBalancerIP: - -resources: {} - #requests: - # cpu: 100m - # memory: 64Mi - #limits: - # cpu: 800m - # memory: 512Mi - -securityContext: - allowPrivilegeEscalation: false - runAsUser: 1000 - runAsGroup: 1000 - fsGroup: 1000 - -# See https://github.com/bitnami/charts/tree/master/bitnami/postgresql -postgresql: - image: - tag: 13 - auth: - username: kemal - password: kemal - database: invidious - primary: - initdb: - username: kemal - password: kemal - scriptsConfigMap: invidious-postgresql-init - -# Adapted from ../config/config.yml -config: - channel_threads: 1 - feed_threads: 1 - db: - user: kemal - password: kemal - host: invidious-postgresql - port: 5432 - dbname: invidious - full_refresh: false - https_only: false - domain: From 31ad708206dc108714e36f617c6bce5f85c80b8b Mon Sep 17 00:00:00 2001 From: meatball Date: Thu, 30 May 2024 21:56:33 +0200 Subject: [PATCH 031/136] 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 032/136] 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 033/136] 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 034/136] Make solution complaint with spec --- src/invidious/videos.cr | 2 +- src/invidious/videos/parser.cr | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index cdfca02c..5a4a55c3 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -250,7 +250,7 @@ struct Video end def genre_url : String? - info["genreUcid"].try &.as_s? ? "/channel/#{info["genreUcid"]}" : nil + info["genreUcid"]? == "" ? nil : "/channel/#{info["genreUcid"]}" end def is_vr : Bool? diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr index 4bdb2512..0e1a947c 100644 --- a/src/invidious/videos/parser.cr +++ b/src/invidious/videos/parser.cr @@ -424,7 +424,7 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any "shortDescription" => JSON::Any.new(short_description.try &.as_s || nil), # Video metadata "genre" => JSON::Any.new(genre.try &.as_s || ""), - "genreUcid" => JSON::Any.new(genre_ucid.try &.as_s?), + "genreUcid" => JSON::Any.new(genre_ucid.try &.as_s || ""), "license" => JSON::Any.new(license.try &.as_s || ""), # Music section "music" => JSON.parse(music_list.to_json), From 0224162ad22dc19d58a73202d796eb3e99f0a71c Mon Sep 17 00:00:00 2001 From: syeopite Date: Tue, 11 Jun 2024 17:57:33 -0700 Subject: [PATCH 035/136] Rewrite transcript logic to be more generic The transcript logic in Invidious was written specifically as a workaround for captions, and not transcripts as a feature. This commit genericises the logic a bit as so it can be used for implementing transcripts within Invidious' API and UI as well. The most notable change is the added parsing of section headings when it was previously skipped over in favor of regular lines. --- src/invidious/routes/api/v1/videos.cr | 9 ++- src/invidious/videos/transcript.cr | 90 +++++++++++++++++---------- 2 files changed, 63 insertions(+), 36 deletions(-) diff --git a/src/invidious/routes/api/v1/videos.cr b/src/invidious/routes/api/v1/videos.cr index 9281f4dd..faff2f59 100644 --- a/src/invidious/routes/api/v1/videos.cr +++ b/src/invidious/routes/api/v1/videos.cr @@ -89,9 +89,14 @@ module Invidious::Routes::API::V1::Videos if CONFIG.use_innertube_for_captions params = Invidious::Videos::Transcript.generate_param(id, caption.language_code, caption.auto_generated) - initial_data = YoutubeAPI.get_transcript(params) - webvtt = Invidious::Videos::Transcript.convert_transcripts_to_vtt(initial_data, caption.language_code) + transcript = Invidious::Videos::Transcript.from_raw( + YoutubeAPI.get_transcript(params), + caption.language_code, + caption.auto_generated + ) + + webvtt = transcript.to_vtt else # Timedtext API handling url = URI.parse("#{caption.base_url}&tlang=#{tlang}").request_target diff --git a/src/invidious/videos/transcript.cr b/src/invidious/videos/transcript.cr index dac00eea..42f29f46 100644 --- a/src/invidious/videos/transcript.cr +++ b/src/invidious/videos/transcript.cr @@ -1,8 +1,21 @@ module Invidious::Videos - # Namespace for methods primarily relating to Transcripts - module Transcript - record TranscriptLine, start_ms : Time::Span, end_ms : Time::Span, line : String + # A `Transcripts` struct encapsulates a sequence of lines that together forms the whole transcript for a given YouTube video. + # These lines can be categorized into two types: section headings and regular lines representing content from the video. + struct Transcript + # Types + record HeadingLine, start_ms : Time::Span, end_ms : Time::Span, line : String + record RegularLine, start_ms : Time::Span, end_ms : Time::Span, line : String + alias TranscriptLine = HeadingLine | RegularLine + property lines : Array(TranscriptLine) + property language_code : String + property auto_generated : Bool + + # Initializes a new Transcript struct with the contents and associated metadata describing it + def initialize(@lines : Array(TranscriptLine), @language_code : String, @auto_generated : Bool) + end + + # Generates a protobuf string to fetch the requested transcript from YouTube def self.generate_param(video_id : String, language_code : String, auto_generated : Bool) : String kind = auto_generated ? "asr" : "" @@ -30,48 +43,57 @@ module Invidious::Videos return params end - def self.convert_transcripts_to_vtt(initial_data : Hash(String, JSON::Any), target_language : String) : String - # Convert into array of TranscriptLine - lines = self.parse(initial_data) + # Constructs a Transcripts struct from the initial YouTube response + def self.from_raw(initial_data : Hash(String, JSON::Any), language_code : String, auto_generated : Bool) + body = initial_data.dig("actions", 0, "updateEngagementPanelAction", "content", "transcriptRenderer", + "content", "transcriptSearchPanelRenderer", "body", "transcriptSegmentListRenderer", + "initialSegments").as_a + lines = [] of TranscriptLine + + body.each do |line| + if unpacked_line = line["transcriptSectionHeaderRenderer"]? + line_type = HeadingLine + else + unpacked_line = line["transcriptSegmentRenderer"] + line_type = RegularLine + end + + start_ms = unpacked_line["startMs"].as_s.to_i.millisecond + end_ms = unpacked_line["endMs"].as_s.to_i.millisecond + text = extract_text(unpacked_line["snippet"]) || "" + + lines << line_type.new(start_ms, end_ms, text) + end + + return Transcript.new( + lines: lines, + language_code: language_code, + auto_generated: auto_generated, + ) + end + + # Converts transcript lines to a WebVTT file + # + # This is used within Invidious to replace subtitles + # as to workaround YouTube's rate-limited timedtext endpoint. + def to_vtt settings_field = { "Kind" => "captions", - "Language" => target_language, + "Language" => @language_code, } - # Taken from Invidious::Videos::Captions::Metadata.timedtext_to_vtt() vtt = WebVTT.build(settings_field) do |vtt| - lines.each do |line| + @lines.each do |line| + # Section headers are excluded from the VTT conversion as to + # match the regular captions returned from YouTube as much as possible + next if line.is_a? HeadingLine + vtt.cue(line.start_ms, line.end_ms, line.line) end end return vtt end - - private def self.parse(initial_data : Hash(String, JSON::Any)) - body = initial_data.dig("actions", 0, "updateEngagementPanelAction", "content", "transcriptRenderer", - "content", "transcriptSearchPanelRenderer", "body", "transcriptSegmentListRenderer", - "initialSegments").as_a - - lines = [] of TranscriptLine - body.each do |line| - # Transcript section headers. They are not apart of the captions and as such we can safely skip them. - if line.as_h.has_key?("transcriptSectionHeaderRenderer") - next - end - - line = line["transcriptSegmentRenderer"] - - start_ms = line["startMs"].as_s.to_i.millisecond - end_ms = line["endMs"].as_s.to_i.millisecond - - text = extract_text(line["snippet"]) || "" - - lines << TranscriptLine.new(start_ms, end_ms, text) - end - - return lines - end end end From 5b519123a76879edca3d5fa5cff717b58482e7e5 Mon Sep 17 00:00:00 2001 From: syeopite Date: Tue, 11 Jun 2024 18:46:34 -0700 Subject: [PATCH 036/136] Raise error when transcript does not exist --- src/invidious/videos/transcript.cr | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/invidious/videos/transcript.cr b/src/invidious/videos/transcript.cr index 42f29f46..76fb8610 100644 --- a/src/invidious/videos/transcript.cr +++ b/src/invidious/videos/transcript.cr @@ -45,13 +45,19 @@ module Invidious::Videos # Constructs a Transcripts struct from the initial YouTube response def self.from_raw(initial_data : Hash(String, JSON::Any), language_code : String, auto_generated : Bool) - body = initial_data.dig("actions", 0, "updateEngagementPanelAction", "content", "transcriptRenderer", - "content", "transcriptSearchPanelRenderer", "body", "transcriptSegmentListRenderer", - "initialSegments").as_a + segment_list = initial_data.dig("actions", 0, "updateEngagementPanelAction", "content", "transcriptRenderer", + "content", "transcriptSearchPanelRenderer", "body", "transcriptSegmentListRenderer" + ) + + if !segment_list["initialSegments"]? + raise NotFoundException.new("Requested transcript does not exist") + end + + initial_segments = segment_list["initialSegments"].as_a lines = [] of TranscriptLine - body.each do |line| + initial_segments.each do |line| if unpacked_line = line["transcriptSectionHeaderRenderer"]? line_type = HeadingLine else From f466116cd715120a8acea2c388e306caaf62abb0 Mon Sep 17 00:00:00 2001 From: syeopite Date: Thu, 13 Jun 2024 09:05:47 -0700 Subject: [PATCH 037/136] Extract label for transcript in YouTube response --- src/invidious/videos/transcript.cr | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/src/invidious/videos/transcript.cr b/src/invidious/videos/transcript.cr index 76fb8610..9cd064c5 100644 --- a/src/invidious/videos/transcript.cr +++ b/src/invidious/videos/transcript.cr @@ -8,11 +8,16 @@ module Invidious::Videos alias TranscriptLine = HeadingLine | RegularLine property lines : Array(TranscriptLine) + property language_code : String property auto_generated : Bool + # User friendly label for the current transcript. + # Example: "English (auto-generated)" + property label : String + # Initializes a new Transcript struct with the contents and associated metadata describing it - def initialize(@lines : Array(TranscriptLine), @language_code : String, @auto_generated : Bool) + def initialize(@lines : Array(TranscriptLine), @language_code : String, @auto_generated : Bool, @label : String) end # Generates a protobuf string to fetch the requested transcript from YouTube @@ -45,14 +50,29 @@ module Invidious::Videos # Constructs a Transcripts struct from the initial YouTube response def self.from_raw(initial_data : Hash(String, JSON::Any), language_code : String, auto_generated : Bool) - segment_list = initial_data.dig("actions", 0, "updateEngagementPanelAction", "content", "transcriptRenderer", - "content", "transcriptSearchPanelRenderer", "body", "transcriptSegmentListRenderer" - ) + transcript_panel = initial_data.dig("actions", 0, "updateEngagementPanelAction", "content", "transcriptRenderer", + "content", "transcriptSearchPanelRenderer") + + segment_list = transcript_panel.dig("body", "transcriptSegmentListRenderer") if !segment_list["initialSegments"]? raise NotFoundException.new("Requested transcript does not exist") end + # Extract user-friendly label for the current transcript + + footer_language_menu = transcript_panel.dig?( + "footer", "transcriptFooterRenderer", "languageMenu", "sortFilterSubMenuRenderer", "subMenuItems" + ) + + if footer_language_menu + label = footer_language_menu.as_a.select(&.["selected"].as_bool)[0]["title"].as_s + else + label = language_code + end + + # Extract transcript lines + initial_segments = segment_list["initialSegments"].as_a lines = [] of TranscriptLine @@ -76,6 +96,7 @@ module Invidious::Videos lines: lines, language_code: language_code, auto_generated: auto_generated, + label: label ) end From e82c965e897494cdb200a13407e75973f6ab03c5 Mon Sep 17 00:00:00 2001 From: Fijxu Date: Wed, 5 Jun 2024 11:26:57 -0400 Subject: [PATCH 038/136] Player: Fix video playback for videos that have already been watched. Trying to watch an already watched video will make the video start 15 seconds before the end of the video. This is not very comfortable when listening to music or watching/listening playlists over and over. --- assets/js/player.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/assets/js/player.js b/assets/js/player.js index 71c5e7da..d32062c6 100644 --- a/assets/js/player.js +++ b/assets/js/player.js @@ -351,7 +351,12 @@ if (video_data.params.save_player_pos) { const rememberedTime = get_video_time(); let lastUpdated = 0; - if(!hasTimeParam) set_seconds_after_start(rememberedTime); + if(!hasTimeParam) { + if (rememberedTime >= video_data.length_seconds - 20) + set_seconds_after_start(0); + else + set_seconds_after_start(rememberedTime); + } player.on('timeupdate', function () { const raw = player.currentTime(); From 45fd4a1968e7c19b1366eb6c05f370efbd2756cd Mon Sep 17 00:00:00 2001 From: syeopite Date: Sun, 16 Jun 2024 13:11:48 -0700 Subject: [PATCH 039/136] Add job to lint code through Ameba in CI --- .github/workflows/ci.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 057e4d61..0db0cb75 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -124,4 +124,26 @@ jobs: - name: Test Docker run: while curl -Isf http://localhost:3000; do sleep 1; done + ameba_lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - name: Install Crystal + uses: crystal-lang/install-crystal@v1.8.0 + with: + crystal: latest + + - name: Cache Shards + uses: actions/cache@v3 + with: + path: ./lib + key: shards-${{ hashFiles('shard.lock') }} + + - name: Install Shards + run: shards install + + - name: Run Ameba linter + run: bin/ameba From a644d76497ad48baf7a5d0230151fdcc4eb33414 Mon Sep 17 00:00:00 2001 From: syeopite Date: Sun, 16 Jun 2024 13:12:15 -0700 Subject: [PATCH 040/136] Update ameba config --- .ameba.yml | 58 +++++++++++------------------------------------------- 1 file changed, 11 insertions(+), 47 deletions(-) diff --git a/.ameba.yml b/.ameba.yml index 96cbc8f0..c7629dcb 100644 --- a/.ameba.yml +++ b/.ameba.yml @@ -20,6 +20,9 @@ Lint/ShadowingOuterLocalVar: Excluded: - src/invidious/helpers/tokens.cr +Lint/NotNil: + Enabled: false + # # Style @@ -31,6 +34,13 @@ Style/RedundantBegin: Style/RedundantReturn: Enabled: false +Style/ParenthesesAroundCondition: + Enabled: false + +# This requires a rewrite of most data structs (and their usage) in Invidious. +Style/QueryBoolMethods: + Enabled: false + # # Metrics @@ -39,50 +49,4 @@ Style/RedundantReturn: # Ignore function complexity (number of if/else & case/when branches) # For some functions that can hardly be simplified for now Metrics/CyclomaticComplexity: - Excluded: - # get_about_info(ucid, locale) => [17/10] - - src/invidious/channels/about.cr - - # fetch_channel_community(ucid, continuation, ...) => [34/10] - - src/invidious/channels/community.cr - - # create_notification_stream(env, topics, connection_channel) => [14/10] - - src/invidious/helpers/helpers.cr:84:5 - - # get_index(plural_form, count) => [25/10] - - src/invidious/helpers/i18next.cr - - # call(context) => [18/10] - - src/invidious/helpers/static_file_handler.cr - - # show(env) => [38/10] - - src/invidious/routes/embed.cr - - # get_video_playback(env) => [45/10] - - src/invidious/routes/video_playback.cr - - # handle(env) => [40/10] - - src/invidious/routes/watch.cr - - # playlist_ajax(env) => [24/10] - - src/invidious/routes/playlists.cr - - # fetch_youtube_comments(id, cursor, ....) => [40/10] - # template_youtube_comments(comments, locale, ...) => [16/10] - # content_to_comment_html(content) => [14/10] - - src/invidious/comments.cr - - # to_json(locale, json) => [21/10] - # extract_video_info(video_id, ...) => [44/10] - # process_video_params(query, preferences) => [20/10] - - src/invidious/videos.cr - - - -#src/invidious/playlists.cr:327:5 -#[C] Metrics/CyclomaticComplexity: Cyclomatic complexity too high [19/10] -# fetch_playlist(plid : String) - -#src/invidious/playlists.cr:436:5 -#[C] Metrics/CyclomaticComplexity: Cyclomatic complexity too high [11/10] -# extract_playlist_videos(initial_data : Hash(String, JSON::Any)) + Enabled: false From e0ed094cc46c6e3e7b37e5e3fbfc8bea9bc267a6 Mon Sep 17 00:00:00 2001 From: syeopite Date: Sun, 16 Jun 2024 13:29:06 -0700 Subject: [PATCH 041/136] Cache ameba binary --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0db0cb75..eb18f639 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -139,7 +139,9 @@ jobs: - name: Cache Shards uses: actions/cache@v3 with: - path: ./lib + path: | + ./lib + ./bin key: shards-${{ hashFiles('shard.lock') }} - name: Install Shards From 6b429575bfa8adcdf2a57a09312c7700237d8a13 Mon Sep 17 00:00:00 2001 From: syeopite Date: Sun, 16 Jun 2024 16:22:01 -0700 Subject: [PATCH 042/136] Update ameba version --- shard.lock | 2 +- shard.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/shard.lock b/shard.lock index efb60a59..397bd8bc 100644 --- a/shard.lock +++ b/shard.lock @@ -2,7 +2,7 @@ version: 2.0 shards: ameba: git: https://github.com/crystal-ameba/ameba.git - version: 1.5.0 + version: 1.6.1 athena-negotiation: git: https://github.com/athena-framework/negotiation.git diff --git a/shard.yml b/shard.yml index be06a7df..367f7c73 100644 --- a/shard.yml +++ b/shard.yml @@ -35,7 +35,7 @@ development_dependencies: version: ~> 0.10.4 ameba: github: crystal-ameba/ameba - version: ~> 1.5.0 + version: ~> 1.6.1 crystal: ">= 1.0.0, < 2.0.0" From 248df785d764023d8ffcdfa8cad08c17a12fe7a6 Mon Sep 17 00:00:00 2001 From: meatball Date: Tue, 18 Jun 2024 20:55:14 +0200 Subject: [PATCH 043/136] 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 044/136] Call `as?` instead of `as` to not force string conversion --- spec/invidious/videos/regular_videos_extract_spec.cr | 4 ++-- spec/invidious/videos/scheduled_live_extract_spec.cr | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/invidious/videos/regular_videos_extract_spec.cr b/spec/invidious/videos/regular_videos_extract_spec.cr index b35738f4..c647c1d1 100644 --- a/spec/invidious/videos/regular_videos_extract_spec.cr +++ b/spec/invidious/videos/regular_videos_extract_spec.cr @@ -67,7 +67,7 @@ Spectator.describe "parse_video_info" do # Video metadata expect(info["genre"].as_s).to eq("Entertainment") - expect(info["genreUcid"].as_s).to be_nil + expect(info["genreUcid"].as_s?).to be_nil expect(info["license"].as_s).to be_empty # Author infos @@ -151,7 +151,7 @@ Spectator.describe "parse_video_info" do # Video metadata expect(info["genre"].as_s).to eq("Music") - expect(info["genreUcid"].as_s).to be_nil + expect(info["genreUcid"].as_s?).to be_nil expect(info["license"].as_s).to be_empty # Author infos diff --git a/spec/invidious/videos/scheduled_live_extract_spec.cr b/spec/invidious/videos/scheduled_live_extract_spec.cr index 82dd8f00..c3a9b228 100644 --- a/spec/invidious/videos/scheduled_live_extract_spec.cr +++ b/spec/invidious/videos/scheduled_live_extract_spec.cr @@ -94,7 +94,7 @@ Spectator.describe "parse_video_info" do # Video metadata expect(info["genre"].as_s).to eq("Entertainment") - expect(info["genreUcid"].as_s).to be_nil + expect(info["genreUcid"].as_s?).to be_nil expect(info["license"].as_s).to be_empty # Author infos From 933802b897bb64fec2beebabc696aba4921be68d Mon Sep 17 00:00:00 2001 From: syeopite Date: Mon, 24 Jun 2024 11:34:55 -0700 Subject: [PATCH 045/136] Use "master" label for master container build --- .github/workflows/container-release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/container-release.yml b/.github/workflows/container-release.yml index e44ac200..edb03489 100644 --- a/.github/workflows/container-release.yml +++ b/.github/workflows/container-release.yml @@ -58,7 +58,7 @@ jobs: images: quay.io/invidious/invidious tags: | type=sha,format=short,prefix={{date 'YYYY.MM.DD'}}-,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} - type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} + type=raw,value=master,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} labels: | quay.expires-after=12w @@ -83,7 +83,7 @@ jobs: suffix=-arm64 tags: | type=sha,format=short,prefix={{date 'YYYY.MM.DD'}}-,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} - type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} + type=raw,value=master,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} labels: | quay.expires-after=12w From 848ab1e9c866f8e55467697e18a4ef35503cc936 Mon Sep 17 00:00:00 2001 From: syeopite Date: Mon, 24 Jun 2024 11:36:11 -0700 Subject: [PATCH 046/136] Specify which workflow builds from master --- .github/workflows/container-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/container-release.yml b/.github/workflows/container-release.yml index edb03489..55a791b6 100644 --- a/.github/workflows/container-release.yml +++ b/.github/workflows/container-release.yml @@ -1,4 +1,4 @@ -name: Build and release container +name: Build and release container directly from master on: push: From dd38eef41aefd5dd0dc40f21489b9b6cb8269333 Mon Sep 17 00:00:00 2001 From: syeopite Date: Mon, 24 Jun 2024 11:45:00 -0700 Subject: [PATCH 047/136] Add workflow to build container on release --- .github/workflows/release-container.yml | 89 +++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 .github/workflows/release-container.yml diff --git a/.github/workflows/release-container.yml b/.github/workflows/release-container.yml new file mode 100644 index 00000000..9129c699 --- /dev/null +++ b/.github/workflows/release-container.yml @@ -0,0 +1,89 @@ +name: Build and release container + +on: + tags: + - "v*" + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Crystal + uses: crystal-lang/install-crystal@v1.8.2 + with: + crystal: 1.12.2 + + - name: Run lint + run: | + if ! crystal tool format --check; then + crystal tool format + git diff + exit 1 + fi + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + with: + platforms: arm64 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to registry + uses: docker/login-action@v3 + with: + registry: quay.io + username: ${{ secrets.QUAY_USERNAME }} + password: ${{ secrets.QUAY_PASSWORD }} + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: quay.io/invidious/invidious + tags: | + type=semvar,pattern={{version}} + type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} + labels: | + quay.expires-after=12w + + - name: Build and push Docker AMD64 image for Push Event + uses: docker/build-push-action@v5 + with: + context: . + file: docker/Dockerfile + platforms: linux/amd64 + labels: ${{ steps.meta.outputs.labels }} + push: true + tags: ${{ steps.meta.outputs.tags }} + build-args: | + "release=1" + + - name: Docker meta + id: meta-arm64 + uses: docker/metadata-action@v5 + with: + images: quay.io/invidious/invidious + flavor: | + suffix=-arm64 + tags: | + type=semvar,pattern={{version}} + type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} + labels: | + quay.expires-after=12w + + - name: Build and push Docker ARM64 image for Push Event + uses: docker/build-push-action@v5 + with: + context: . + file: docker/Dockerfile.arm64 + platforms: linux/arm64/v8 + labels: ${{ steps.meta-arm64.outputs.labels }} + push: true + tags: ${{ steps.meta-arm64.outputs.tags }} + build-args: | + "release=1" From 8f5c6a602b78e34e42d1c58ed888e9c8a70ddaa7 Mon Sep 17 00:00:00 2001 From: syeopite Date: Mon, 1 Jul 2024 21:35:08 -0700 Subject: [PATCH 048/136] Rename container workflows --- .../{container-release.yml => build-nightly-container.yml} | 0 .../{release-container.yml => build-stable-container.yml} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{container-release.yml => build-nightly-container.yml} (100%) rename .github/workflows/{release-container.yml => build-stable-container.yml} (100%) diff --git a/.github/workflows/container-release.yml b/.github/workflows/build-nightly-container.yml similarity index 100% rename from .github/workflows/container-release.yml rename to .github/workflows/build-nightly-container.yml diff --git a/.github/workflows/release-container.yml b/.github/workflows/build-stable-container.yml similarity index 100% rename from .github/workflows/release-container.yml rename to .github/workflows/build-stable-container.yml From 64d1f26eceb735ab5c96644b6545fe7fe5c2e677 Mon Sep 17 00:00:00 2001 From: syeopite Date: Mon, 1 Jul 2024 21:39:14 -0700 Subject: [PATCH 049/136] Fix trigger for stable container build --- .github/workflows/build-stable-container.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-stable-container.yml b/.github/workflows/build-stable-container.yml index 9129c699..032fd762 100644 --- a/.github/workflows/build-stable-container.yml +++ b/.github/workflows/build-stable-container.yml @@ -1,8 +1,9 @@ name: Build and release container on: - tags: - - "v*" + push: + tags: + - "v*" jobs: release: From aace30b2b47b715021a2ab661f9dec4b727604b8 Mon Sep 17 00:00:00 2001 From: syeopite Date: Thu, 4 Jul 2024 10:11:36 -0700 Subject: [PATCH 050/136] Bump nightly container build workflow crystal ver --- .github/workflows/build-nightly-container.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-nightly-container.yml b/.github/workflows/build-nightly-container.yml index 55a791b6..bee27600 100644 --- a/.github/workflows/build-nightly-container.yml +++ b/.github/workflows/build-nightly-container.yml @@ -24,9 +24,9 @@ jobs: uses: actions/checkout@v4 - name: Install Crystal - uses: crystal-lang/install-crystal@v1.8.0 + uses: crystal-lang/install-crystal@v1.8.2 with: - crystal: 1.9.2 + crystal: 1.12.2 - name: Run lint run: | From 220cc9bd2ff87872763899f730a8ba62d45db7dd Mon Sep 17 00:00:00 2001 From: syeopite Date: Thu, 4 Jul 2024 10:13:01 -0700 Subject: [PATCH 051/136] Typo Co-authored-by: Samantaz Fox --- .github/workflows/build-stable-container.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-stable-container.yml b/.github/workflows/build-stable-container.yml index 032fd762..b5fbc705 100644 --- a/.github/workflows/build-stable-container.yml +++ b/.github/workflows/build-stable-container.yml @@ -47,7 +47,7 @@ jobs: with: images: quay.io/invidious/invidious tags: | - type=semvar,pattern={{version}} + type=semver,pattern={{version}} type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} labels: | quay.expires-after=12w @@ -72,7 +72,7 @@ jobs: flavor: | suffix=-arm64 tags: | - type=semvar,pattern={{version}} + type=semver,pattern={{version}} type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} labels: | quay.expires-after=12w From 911dad69358a299b77e14303e570d48960aa0f1d Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 9 Jul 2024 13:25:18 -0400 Subject: [PATCH 052/136] 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 053/136] 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 054/136] 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 055/136] 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 056/136] 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 057/136] 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 058/136] 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 059/136] 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 060/136] 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 061/136] 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 062/136] 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 063/136] 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 064/136] 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 065/136] 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 066/136] 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 067/136] 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 068/136] 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 069/136] 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 070/136] 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 071/136] 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 072/136] 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 073/136] 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 074/136] 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 075/136] 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 076/136] 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 077/136] 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 078/136] 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 079/136] 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 080/136] 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 081/136] 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 082/136] 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 083/136] 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 084/136] 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 085/136] 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 086/136] 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 087/136] 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 088/136] 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 089/136] 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 090/136] 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 091/136] 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 092/136] 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 093/136] 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 094/136] 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 095/136] 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 096/136] 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 097/136] 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 098/136] 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 099/136] 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 100/136] 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 101/136] 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 102/136] 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 103/136] =?UTF-8?q?Update=20Norwegian=20Bokm=C3=A5l=20tran?= =?UTF-8?q?slation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Hosted Weblate Co-authored-by: Petter Reinholdtsen --- locales/nb-NO.json | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/locales/nb-NO.json b/locales/nb-NO.json index cf0ee286..fed6d73f 100644 --- a/locales/nb-NO.json +++ b/locales/nb-NO.json @@ -21,7 +21,7 @@ "Import and Export Data": "Importer- og eksporter data", "Import": "Importer", "Import Invidious data": "Importer Invidious-JSON-data", - "Import YouTube subscriptions": "Importer YouTube/OPML-abonnementer", + "Import YouTube subscriptions": "Importer YouTube CSV eller OPML-abonnementer", "Import FreeTube subscriptions (.db)": "Importer FreeTube-abonnementer (.db)", "Import NewPipe subscriptions (.json)": "Importer NewPipe-abonnementer (.json)", "Import NewPipe data (.zip)": "Importer NewPipe-data (.zip)", @@ -487,5 +487,12 @@ "playlist_button_add_items": "Legg til videoer", "generic_channels_count": "{{count}} kanal", "generic_channels_count_plural": "{{count}} kanaler", - "Import YouTube watch history (.json)": "Importere YouTube visningshistorikk (.json)" + "Import YouTube watch history (.json)": "Importere YouTube visningshistorikk (.json)", + "carousel_go_to": "GĂ„ til lysark `x`", + "Search for videos": "SĂžk i videoer", + "Answer": "Svar", + "carousel_slide": "Lysark {{current}} av {{total}}", + "carousel_skip": "Hopp over karusellen", + "Add to playlist": "Legg til i spilleliste", + "Add to playlist: ": "Legg til i spilleliste: " } From e319c35f097e08590e705378c7e5b479720deabc Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Tue, 13 Aug 2024 20:56:09 +0200 Subject: [PATCH 104/136] 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 From 96ade642faad7052b0b70171a2c0ac4c09819151 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Sun, 26 Nov 2023 20:24:04 -0500 Subject: [PATCH 105/136] Channel: Render age restricted channels --- src/invidious/channels/about.cr | 170 +++++++++++++----------- src/invidious/playlists.cr | 7 + src/invidious/routes/api/v1/channels.cr | 89 ++++++++++--- src/invidious/routes/channels.cr | 74 ++++++++--- 4 files changed, 221 insertions(+), 119 deletions(-) diff --git a/src/invidious/channels/about.cr b/src/invidious/channels/about.cr index edaf5c12..b3561fcd 100644 --- a/src/invidious/channels/about.cr +++ b/src/invidious/channels/about.cr @@ -15,7 +15,8 @@ record AboutChannel, allowed_regions : Array(String), tabs : Array(String), tags : Array(String), - verified : Bool + verified : Bool, + is_age_gated : Bool def get_about_info(ucid, locale) : AboutChannel begin @@ -45,46 +46,102 @@ def get_about_info(ucid, locale) : AboutChannel end tags = [] of String + tab_names = [] of String + total_views = 0_i64 + joined = Time.unix(0) - if auto_generated - author = initdata["header"]["interactiveTabbedHeaderRenderer"]["title"]["simpleText"].as_s - author_url = initdata["microformat"]["microformatDataRenderer"]["urlCanonical"].as_s - author_thumbnail = initdata["header"]["interactiveTabbedHeaderRenderer"]["boxArt"]["thumbnails"][0]["url"].as_s - - # Raises a KeyError on failure. - banners = initdata["header"]["interactiveTabbedHeaderRenderer"]?.try &.["banner"]?.try &.["thumbnails"]? - banner = banners.try &.[-1]?.try &.["url"].as_s? - - description_base_node = initdata["header"]["interactiveTabbedHeaderRenderer"]["description"] - # some channels have the description in a simpleText - # ex: https://www.youtube.com/channel/UCQvWX73GQygcwXOTSf_VDVg/ - description_node = description_base_node.dig?("simpleText") || description_base_node - - tags = initdata.dig?("header", "interactiveTabbedHeaderRenderer", "badges") - .try &.as_a.map(&.["metadataBadgeRenderer"]["label"].as_s) || [] of String + if ageGate = initdata.dig?("contents", "twoColumnBrowseResultsRenderer", "tabs", 0, "tabRenderer", "content", "sectionListRenderer", "contents", 0, "channelAgeGateRenderer") + description_node = nil + author = ageGate["channelTitle"].as_s + ucid = initdata["responseContext"]["serviceTrackingParams"][0]["params"][0]["value"].as_s + author_url = "https://www.youtube.com/channel/" + ucid + author_thumbnail = ageGate["avatar"]["thumbnails"][0]["url"].as_s + banner = nil + is_family_friendly = false + is_age_gated = true + tab_names = ["videos", "shorts", "streams"] + auto_generated = false else - author = initdata["metadata"]["channelMetadataRenderer"]["title"].as_s - author_url = initdata["metadata"]["channelMetadataRenderer"]["channelUrl"].as_s - author_thumbnail = initdata["metadata"]["channelMetadataRenderer"]["avatar"]["thumbnails"][0]["url"].as_s - author_verified = has_verified_badge?(initdata.dig?("header", "c4TabbedHeaderRenderer", "badges")) + if auto_generated + author = initdata["header"]["interactiveTabbedHeaderRenderer"]["title"]["simpleText"].as_s + author_url = initdata["microformat"]["microformatDataRenderer"]["urlCanonical"].as_s + author_thumbnail = initdata["header"]["interactiveTabbedHeaderRenderer"]["boxArt"]["thumbnails"][0]["url"].as_s - ucid = initdata["metadata"]["channelMetadataRenderer"]["externalId"].as_s + # Raises a KeyError on failure. + banners = initdata["header"]["interactiveTabbedHeaderRenderer"]?.try &.["banner"]?.try &.["thumbnails"]? + banner = banners.try &.[-1]?.try &.["url"].as_s? - # 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? + description_base_node = initdata["header"]["interactiveTabbedHeaderRenderer"]["description"] + # some channels have the description in a simpleText + # ex: https://www.youtube.com/channel/UCQvWX73GQygcwXOTSf_VDVg/ + description_node = description_base_node.dig?("simpleText") || description_base_node - # if banner.includes? "channels/c4/default_banner" - # banner = nil - # end + tags = initdata.dig?("header", "interactiveTabbedHeaderRenderer", "badges") + .try &.as_a.map(&.["metadataBadgeRenderer"]["label"].as_s) || [] of String + else + author = initdata["metadata"]["channelMetadataRenderer"]["title"].as_s + author_url = initdata["metadata"]["channelMetadataRenderer"]["channelUrl"].as_s + author_thumbnail = initdata["metadata"]["channelMetadataRenderer"]["avatar"]["thumbnails"][0]["url"].as_s + author_verified = has_verified_badge?(initdata.dig?("header", "c4TabbedHeaderRenderer", "badges")) - description_node = initdata["metadata"]["channelMetadataRenderer"]?.try &.["description"]? - tags = initdata.dig?("microformat", "microformatDataRenderer", "tags").try &.as_a.map(&.as_s) || [] of String + ucid = initdata["metadata"]["channelMetadataRenderer"]["externalId"].as_s + + # 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" + # banner = nil + # end + + description_node = initdata["metadata"]["channelMetadataRenderer"]?.try &.["description"]? + tags = initdata.dig?("microformat", "microformatDataRenderer", "tags").try &.as_a.map(&.as_s) || [] of String + end + + is_family_friendly = initdata["microformat"]["microformatDataRenderer"]["familySafe"].as_bool + if tabs_json = initdata["contents"]["twoColumnBrowseResultsRenderer"]["tabs"]? + # Get the name of the tabs available on this channel + tab_names = tabs_json.as_a.compact_map do |entry| + name = entry.dig?("tabRenderer", "title").try &.as_s.downcase + + # This is a small fix to not add extra code on the HTML side + # I.e, the URL for the "live" tab is .../streams, so use "streams" + # everywhere for the sake of simplicity + (name == "live") ? "streams" : name + end + + # Get the currently active tab ("About") + about_tab = extract_selected_tab(tabs_json) + + # Try to find the about metadata section + channel_about_meta = about_tab.dig?( + "content", + "sectionListRenderer", "contents", 0, + "itemSectionRenderer", "contents", 0, + "channelAboutFullMetadataRenderer" + ) + + if !channel_about_meta.nil? + total_views = channel_about_meta.dig?("viewCountText", "simpleText").try &.as_s.gsub(/\D/, "").to_i64? || 0_i64 + + # The joined text is split to several sub strings. The reduce joins those strings before parsing the date. + joined = extract_text(channel_about_meta["joinedDateText"]?) + .try { |text| Time.parse(text, "Joined %b %-d, %Y", Time::Location.local) } || Time.unix(0) + + # Normal Auto-generated channels + # https://support.google.com/youtube/answer/2579942 + # For auto-generated channels, channel_about_meta only has + # ["description"]["simpleText"] and ["primaryLinks"][0]["title"]["simpleText"] + auto_generated = ( + (channel_about_meta["primaryLinks"]?.try &.size) == 1 && \ + extract_text(channel_about_meta.dig?("primaryLinks", 0, "title")) == "Auto-generated by YouTube" || + channel_about_meta.dig?("links", 0, "channelExternalLinkViewModel", "title", "content").try &.as_s == "Auto-generated by YouTube" + ) + end + end end - is_family_friendly = initdata["microformat"]["microformatDataRenderer"]["familySafe"].as_bool - allowed_regions = initdata .dig?("microformat", "microformatDataRenderer", "availableCountries") .try &.as_a.map(&.as_s) || [] of String @@ -102,52 +159,6 @@ def get_about_info(ucid, locale) : AboutChannel end end - total_views = 0_i64 - joined = Time.unix(0) - - tab_names = [] of String - - if tabs_json = initdata["contents"]["twoColumnBrowseResultsRenderer"]["tabs"]? - # Get the name of the tabs available on this channel - tab_names = tabs_json.as_a.compact_map do |entry| - name = entry.dig?("tabRenderer", "title").try &.as_s.downcase - - # This is a small fix to not add extra code on the HTML side - # I.e, the URL for the "live" tab is .../streams, so use "streams" - # everywhere for the sake of simplicity - (name == "live") ? "streams" : name - end - - # Get the currently active tab ("About") - about_tab = extract_selected_tab(tabs_json) - - # Try to find the about metadata section - channel_about_meta = about_tab.dig?( - "content", - "sectionListRenderer", "contents", 0, - "itemSectionRenderer", "contents", 0, - "channelAboutFullMetadataRenderer" - ) - - if !channel_about_meta.nil? - total_views = channel_about_meta.dig?("viewCountText", "simpleText").try &.as_s.gsub(/\D/, "").to_i64? || 0_i64 - - # The joined text is split to several sub strings. The reduce joins those strings before parsing the date. - joined = extract_text(channel_about_meta["joinedDateText"]?) - .try { |text| Time.parse(text, "Joined %b %-d, %Y", Time::Location.local) } || Time.unix(0) - - # Normal Auto-generated channels - # https://support.google.com/youtube/answer/2579942 - # For auto-generated channels, channel_about_meta only has - # ["description"]["simpleText"] and ["primaryLinks"][0]["title"]["simpleText"] - auto_generated = ( - (channel_about_meta["primaryLinks"]?.try &.size) == 1 && \ - extract_text(channel_about_meta.dig?("primaryLinks", 0, "title")) == "Auto-generated by YouTube" || - channel_about_meta.dig?("links", 0, "channelExternalLinkViewModel", "title", "content").try &.as_s == "Auto-generated by YouTube" - ) - end - end - sub_count = 0 if (metadata_rows = initdata.dig?("header", "pageHeaderRenderer", "content", "pageHeaderViewModel", "metadata", "contentMetadataViewModel", "metadataRows").try &.as_a) @@ -177,6 +188,7 @@ def get_about_info(ucid, locale) : AboutChannel tabs: tab_names, tags: tags, verified: author_verified || false, + is_age_gated: is_age_gated || false, ) end diff --git a/src/invidious/playlists.cr b/src/invidious/playlists.cr index a227f794..3e6eef95 100644 --- a/src/invidious/playlists.cr +++ b/src/invidious/playlists.cr @@ -46,8 +46,14 @@ struct PlaylistVideo XML.build { |xml| to_xml(xml) } end + def to_json(locale : String?, json : JSON::Builder) + to_json(json) + end + def to_json(json : JSON::Builder, index : Int32? = nil) json.object do + json.field "type", "video" + json.field "title", self.title json.field "videoId", self.id @@ -67,6 +73,7 @@ struct PlaylistVideo end json.field "lengthSeconds", self.length_seconds + json.field "liveNow", self.live_now end end diff --git a/src/invidious/routes/api/v1/channels.cr b/src/invidious/routes/api/v1/channels.cr index 43a5c35b..2da76134 100644 --- a/src/invidious/routes/api/v1/channels.cr +++ b/src/invidious/routes/api/v1/channels.cr @@ -27,10 +27,21 @@ module Invidious::Routes::API::V1::Channels # Retrieve "sort by" setting from URL parameters sort_by = env.params.query["sort_by"]?.try &.downcase || "newest" - begin - videos, _ = Channel::Tabs.get_videos(channel, sort_by: sort_by) - rescue ex - return error_json(500, ex) + if channel.is_age_gated + begin + playlist = get_playlist(channel.ucid.sub("UC", "UULF")) + videos = get_playlist_videos(playlist, offset: 0) + rescue ex : InfoException + # playlist doesnt exist. + videos = [] of PlaylistVideo + end + next_continuation = nil + else + begin + videos, _ = Channel::Tabs.get_videos(channel, sort_by: sort_by) + rescue ex + return error_json(500, ex) + end end JSON.build do |json| @@ -84,6 +95,7 @@ module Invidious::Routes::API::V1::Channels json.field "joined", channel.joined.to_unix json.field "autoGenerated", channel.auto_generated + json.field "ageGated", channel.is_age_gated json.field "isFamilyFriendly", channel.is_family_friendly json.field "description", html_to_content(channel.description_html) json.field "descriptionHtml", channel.description_html @@ -142,12 +154,23 @@ module Invidious::Routes::API::V1::Channels sort_by = env.params.query["sort_by"]?.try &.downcase || "newest" continuation = env.params.query["continuation"]? - begin - videos, next_continuation = Channel::Tabs.get_60_videos( - channel, continuation: continuation, sort_by: sort_by - ) - rescue ex - return error_json(500, ex) + if channel.is_age_gated + begin + playlist = get_playlist(channel.ucid.sub("UC", "UULF")) + videos = get_playlist_videos(playlist, offset: 0) + rescue ex : InfoException + # playlist doesnt exist. + videos = [] of PlaylistVideo + end + next_continuation = nil + else + begin + videos, next_continuation = Channel::Tabs.get_60_videos( + channel, continuation: continuation, sort_by: sort_by + ) + rescue ex + return error_json(500, ex) + end end return JSON.build do |json| @@ -176,12 +199,23 @@ module Invidious::Routes::API::V1::Channels # Retrieve continuation from URL parameters continuation = env.params.query["continuation"]? - begin - videos, next_continuation = Channel::Tabs.get_shorts( - channel, continuation: continuation - ) - rescue ex - return error_json(500, ex) + if channel.is_age_gated + begin + playlist = get_playlist(channel.ucid.sub("UC", "UUSH")) + videos = get_playlist_videos(playlist, offset: 0) + rescue ex : InfoException + # playlist doesnt exist. + videos = [] of PlaylistVideo + end + next_continuation = nil + else + begin + videos, next_continuation = Channel::Tabs.get_shorts( + channel, continuation: continuation + ) + rescue ex + return error_json(500, ex) + end end return JSON.build do |json| @@ -211,12 +245,23 @@ module Invidious::Routes::API::V1::Channels sort_by = env.params.query["sort_by"]?.try &.downcase || "newest" continuation = env.params.query["continuation"]? - begin - videos, next_continuation = Channel::Tabs.get_60_livestreams( - channel, continuation: continuation, sort_by: sort_by - ) - rescue ex - return error_json(500, ex) + if channel.is_age_gated + begin + playlist = get_playlist(channel.ucid.sub("UC", "UULV")) + videos = get_playlist_videos(playlist, offset: 0) + rescue ex : InfoException + # playlist doesnt exist. + videos = [] of PlaylistVideo + end + next_continuation = nil + else + begin + videos, next_continuation = Channel::Tabs.get_60_livestreams( + channel, continuation: continuation, sort_by: sort_by + ) + rescue ex + return error_json(500, ex) + end end return JSON.build do |json| diff --git a/src/invidious/routes/channels.cr b/src/invidious/routes/channels.cr index 360af2cd..952098e0 100644 --- a/src/invidious/routes/channels.cr +++ b/src/invidious/routes/channels.cr @@ -36,12 +36,24 @@ module Invidious::Routes::Channels items = items.select(SearchPlaylist) items.each(&.author = "") else - sort_options = {"newest", "oldest", "popular"} - # Fetch items and continuation token - items, next_continuation = Channel::Tabs.get_videos( - channel, continuation: continuation, sort_by: (sort_by || "newest") - ) + if channel.is_age_gated + sort_by = "" + sort_options = [] of String + begin + playlist = get_playlist(channel.ucid.sub("UC", "UULF")) + items = get_playlist_videos(playlist, offset: 0) + rescue ex : InfoException + # playlist doesnt exist. + items = [] of PlaylistVideo + end + next_continuation = nil + else + sort_options = {"newest", "oldest", "popular"} + items, next_continuation = Channel::Tabs.get_videos( + channel, continuation: continuation, sort_by: (sort_by || "newest") + ) + end end selected_tab = Frontend::ChannelPage::TabsAvailable::Videos @@ -58,14 +70,27 @@ module Invidious::Routes::Channels return env.redirect "/channel/#{channel.ucid}" end - # TODO: support sort option for shorts - sort_by = "" - sort_options = [] of String + if channel.is_age_gated + sort_by = "" + sort_options = [] of String + begin + playlist = get_playlist(channel.ucid.sub("UC", "UUSH")) + items = get_playlist_videos(playlist, offset: 0) + rescue ex : InfoException + # playlist doesnt exist. + items = [] of PlaylistVideo + end + next_continuation = nil + else + # TODO: support sort option for shorts + sort_by = "" + sort_options = [] of String - # Fetch items and continuation token - items, next_continuation = Channel::Tabs.get_shorts( - channel, continuation: continuation - ) + # Fetch items and continuation token + items, next_continuation = Channel::Tabs.get_shorts( + channel, continuation: continuation + ) + end selected_tab = Frontend::ChannelPage::TabsAvailable::Shorts templated "channel" @@ -81,13 +106,26 @@ module Invidious::Routes::Channels return env.redirect "/channel/#{channel.ucid}" end - sort_by = env.params.query["sort_by"]?.try &.downcase || "newest" - sort_options = {"newest", "oldest", "popular"} + if channel.is_age_gated + sort_by = "" + sort_options = [] of String + begin + playlist = get_playlist(channel.ucid.sub("UC", "UULV")) + items = get_playlist_videos(playlist, offset: 0) + rescue ex : InfoException + # playlist doesnt exist. + items = [] of PlaylistVideo + end + next_continuation = nil + else + sort_by = env.params.query["sort_by"]?.try &.downcase || "newest" + sort_options = {"newest", "oldest", "popular"} - # Fetch items and continuation token - items, next_continuation = Channel::Tabs.get_60_livestreams( - channel, continuation: continuation, sort_by: sort_by - ) + # Fetch items and continuation token + items, next_continuation = Channel::Tabs.get_60_livestreams( + channel, continuation: continuation, sort_by: sort_by + ) + end selected_tab = Frontend::ChannelPage::TabsAvailable::Streams templated "channel" From e31053e812517d8d097368ae8863404a4a563731 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Sun, 19 May 2024 10:46:05 -0400 Subject: [PATCH 106/136] Use dig to get properties Co-authored-by: Samantaz Fox --- src/invidious/channels/about.cr | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/invidious/channels/about.cr b/src/invidious/channels/about.cr index b3561fcd..1380044a 100644 --- a/src/invidious/channels/about.cr +++ b/src/invidious/channels/about.cr @@ -53,9 +53,9 @@ def get_about_info(ucid, locale) : AboutChannel if ageGate = initdata.dig?("contents", "twoColumnBrowseResultsRenderer", "tabs", 0, "tabRenderer", "content", "sectionListRenderer", "contents", 0, "channelAgeGateRenderer") description_node = nil author = ageGate["channelTitle"].as_s - ucid = initdata["responseContext"]["serviceTrackingParams"][0]["params"][0]["value"].as_s - author_url = "https://www.youtube.com/channel/" + ucid - author_thumbnail = ageGate["avatar"]["thumbnails"][0]["url"].as_s + ucid = initdata.dig("responseContext", "serviceTrackingParams", 0, "params", 0, "value").as_s + author_url = "https://www.youtube.com/channel/#{ucid}" + author_thumbnail = ageGate.dig("avatar", "thumbnails", 0, "url").as_s banner = nil is_family_friendly = false is_age_gated = true From 466bfbb30637b625ceda1e1073dbc190e51c8dc9 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Wed, 14 Aug 2024 21:43:37 +0200 Subject: [PATCH 107/136] SigHelper: Fix inverted time comparison in 'check_update' --- src/invidious/helpers/signatures.cr | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/invidious/helpers/signatures.cr b/src/invidious/helpers/signatures.cr index 84a8a86d..82a28fc0 100644 --- a/src/invidious/helpers/signatures.cr +++ b/src/invidious/helpers/signatures.cr @@ -10,10 +10,8 @@ class Invidious::DecryptFunction end def check_update - now = Time.utc - # If we have updated in the last 5 minutes, do nothing - return if (now - @last_update) > 5.minutes + return if (Time.utc - @last_update) < 5.minutes # Get the amount of time elapsed since when the player was updated, in the # event where multiple invidious processes are run in parallel. From acbb62586611ec8fd25df9b56f2042a830933155 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Thu, 15 Aug 2024 12:54:43 +0200 Subject: [PATCH 108/136] YtAPI: Update clients to latest version --- src/invidious/yt_backend/youtube_api.cr | 26 ++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/invidious/yt_backend/youtube_api.cr b/src/invidious/yt_backend/youtube_api.cr index 6d585bf2..d66bf7aa 100644 --- a/src/invidious/yt_backend/youtube_api.cr +++ b/src/invidious/yt_backend/youtube_api.cr @@ -6,10 +6,10 @@ module YoutubeAPI extend self # For Android versions, see https://en.wikipedia.org/wiki/Android_version_history - private ANDROID_APP_VERSION = "19.14.42" - private ANDROID_USER_AGENT = "com.google.android.youtube/19.14.42 (Linux; U; Android 12; US) gzip" - private ANDROID_SDK_VERSION = 31_i64 + private ANDROID_APP_VERSION = "19.32.34" private ANDROID_VERSION = "12" + private ANDROID_USER_AGENT = "com.google.android.youtube/#{ANDROID_APP_VERSION} (Linux; U; Android #{ANDROID_VERSION}; US) gzip" + private ANDROID_SDK_VERSION = 31_i64 private ANDROID_TS_APP_VERSION = "1.9" private ANDROID_TS_USER_AGENT = "com.google.android.youtube/1.9 (Linux; U; Android 12; US) gzip" @@ -17,9 +17,9 @@ module YoutubeAPI # For Apple device names, see https://gist.github.com/adamawolf/3048717 # For iOS versions, see https://en.wikipedia.org/wiki/IOS_version_history#Releases, # then go to the dedicated article of the major version you want. - private IOS_APP_VERSION = "19.16.3" - private IOS_USER_AGENT = "com.google.ios.youtube/19.16.3 (iPhone14,5; U; CPU iOS 17_4 like Mac OS X;)" - private IOS_VERSION = "17.4.0.21E219" # Major.Minor.Patch.Build + private IOS_APP_VERSION = "19.32.8" + private IOS_USER_AGENT = "com.google.ios.youtube/#{IOS_APP_VERSION} (iPhone14,5; U; CPU iOS 17_6 like Mac OS X;)" + private IOS_VERSION = "17.6.1.21G93" # Major.Minor.Patch.Build private WINDOWS_VERSION = "10.0" @@ -48,7 +48,7 @@ module YoutubeAPI ClientType::Web => { name: "WEB", name_proto: "1", - version: "2.20240304.00.00", + version: "2.20240814.00.00", screen: "WATCH_FULL_SCREEN", os_name: "Windows", os_version: WINDOWS_VERSION, @@ -57,7 +57,7 @@ module YoutubeAPI ClientType::WebEmbeddedPlayer => { name: "WEB_EMBEDDED_PLAYER", name_proto: "56", - version: "1.20240303.00.00", + version: "1.20240812.01.00", screen: "EMBED", os_name: "Windows", os_version: WINDOWS_VERSION, @@ -66,7 +66,7 @@ module YoutubeAPI ClientType::WebMobile => { name: "MWEB", name_proto: "2", - version: "2.20240304.08.00", + version: "2.20240813.02.00", os_name: "Android", os_version: ANDROID_VERSION, platform: "MOBILE", @@ -74,7 +74,7 @@ module YoutubeAPI ClientType::WebScreenEmbed => { name: "WEB", name_proto: "1", - version: "2.20240304.00.00", + version: "2.20240814.00.00", screen: "EMBED", os_name: "Windows", os_version: WINDOWS_VERSION, @@ -147,8 +147,8 @@ module YoutubeAPI ClientType::IOSMusic => { name: "IOS_MUSIC", name_proto: "26", - version: "6.42", - user_agent: "com.google.ios.youtubemusic/6.42 (iPhone14,5; U; CPU iOS 17_4 like Mac OS X;)", + version: "7.14", + user_agent: "com.google.ios.youtubemusic/7.14 (iPhone14,5; U; CPU iOS 17_6 like Mac OS X;)", device_make: "Apple", device_model: "iPhone14,5", os_name: "iPhone", @@ -161,7 +161,7 @@ module YoutubeAPI ClientType::TvHtml5 => { name: "TVHTML5", name_proto: "7", - version: "7.20240304.10.00", + version: "7.20240813.07.00", }, ClientType::TvHtml5ScreenEmbed => { name: "TVHTML5_SIMPLY_EMBEDDED_PLAYER", From cc33d3f074c24be8b9afac5ddbc0465a87f0d867 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Thu, 15 Aug 2024 18:13:41 +0200 Subject: [PATCH 109/136] YtAPI: Also update User-Agent string --- src/invidious/yt_backend/connection_pool.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/yt_backend/connection_pool.cr b/src/invidious/yt_backend/connection_pool.cr index 0ac785e6..ca612083 100644 --- a/src/invidious/yt_backend/connection_pool.cr +++ b/src/invidious/yt_backend/connection_pool.cr @@ -1,6 +1,6 @@ def add_yt_headers(request) request.headers.delete("User-Agent") if request.headers["User-Agent"] == "Crystal" - request.headers["User-Agent"] ||= "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + request.headers["User-Agent"] ||= "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36" request.headers["Accept-Charset"] ||= "ISO-8859-1,utf-8;q=0.7,*;q=0.7" request.headers["Accept"] ||= "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" From 0b28054f8ac4066d5f2966a75a92eb935247d737 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Thu, 15 Aug 2024 18:26:17 +0200 Subject: [PATCH 110/136] videos: Fix XSS vulnerability in description/comments Patch provided by e-mail, thanks to an anonymous user whose cats are named Yoshi and Yasuo. Comment is mine --- src/invidious/videos/description.cr | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/invidious/videos/description.cr b/src/invidious/videos/description.cr index c7191dec..1371bebb 100644 --- a/src/invidious/videos/description.cr +++ b/src/invidious/videos/description.cr @@ -36,7 +36,13 @@ def parse_description(desc, video_id : String) : String? return "" if content.empty? commands = desc["commandRuns"]?.try &.as_a - return content if commands.nil? + if commands.nil? + # Slightly faster than HTML.escape, as we're only doing one pass on + # the string instead of five for the standard library + return String.build do |str| + copy_string(str, content.each_codepoint, content.size) + end + end # Not everything is stored in UTF-8 on youtube's side. The SMP codepoints # (0x10000 and above) are encoded as UTF-16 surrogate pairs, which are From 6878822c4d621bc2a2ba65c117efc65246e9a1ca Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sun, 8 Oct 2023 12:58:04 +0200 Subject: [PATCH 111/136] Storyboards: Move parser to its own file --- src/invidious/videos.cr | 61 +------------------------- src/invidious/videos/storyboard.cr | 69 ++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 59 deletions(-) create mode 100644 src/invidious/videos/storyboard.cr diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index 6d0cf9ba..73321909 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -177,65 +177,8 @@ struct Video # Misc. methods def storyboards - storyboards = info.dig?("storyboards", "playerStoryboardSpecRenderer", "spec") - .try &.as_s.split("|") - - if !storyboards - if storyboard = info.dig?("storyboards", "playerLiveStoryboardSpecRenderer", "spec").try &.as_s - return [{ - url: storyboard.split("#")[0], - width: 106, - height: 60, - count: -1, - interval: 5000, - storyboard_width: 3, - storyboard_height: 3, - storyboard_count: -1, - }] - end - end - - items = [] of NamedTuple( - url: String, - width: Int32, - height: Int32, - count: Int32, - interval: Int32, - storyboard_width: Int32, - storyboard_height: Int32, - storyboard_count: Int32) - - return items if !storyboards - - url = URI.parse(storyboards.shift) - params = HTTP::Params.parse(url.query || "") - - storyboards.each_with_index do |sb, i| - width, height, count, storyboard_width, storyboard_height, interval, _, sigh = sb.split("#") - params["sigh"] = sigh - url.query = params.to_s - - width = width.to_i - height = height.to_i - count = count.to_i - interval = interval.to_i - storyboard_width = storyboard_width.to_i - storyboard_height = storyboard_height.to_i - storyboard_count = (count / (storyboard_width * storyboard_height)).ceil.to_i - - items << { - url: url.to_s.sub("$L", i).sub("$N", "M$M"), - width: width, - height: height, - count: count, - interval: interval, - storyboard_width: storyboard_width, - storyboard_height: storyboard_height, - storyboard_count: storyboard_count, - } - end - - items + container = info.dig?("storyboards") || JSON::Any.new("{}") + return IV::Videos::Storyboard.from_yt_json(container) end def paid diff --git a/src/invidious/videos/storyboard.cr b/src/invidious/videos/storyboard.cr new file mode 100644 index 00000000..b4302d88 --- /dev/null +++ b/src/invidious/videos/storyboard.cr @@ -0,0 +1,69 @@ +require "uri" +require "http/params" + +module Invidious::Videos + struct Storyboard + # Parse the JSON structure from Youtube + def self.from_yt_json(container : JSON::Any) + storyboards = container.dig?("playerStoryboardSpecRenderer", "spec") + .try &.as_s.split("|") + + if !storyboards + if storyboard = container.dig?("playerLiveStoryboardSpecRenderer", "spec").try &.as_s + return [{ + url: storyboard.split("#")[0], + width: 106, + height: 60, + count: -1, + interval: 5000, + storyboard_width: 3, + storyboard_height: 3, + storyboard_count: -1, + }] + end + end + + items = [] of NamedTuple( + url: String, + width: Int32, + height: Int32, + count: Int32, + interval: Int32, + storyboard_width: Int32, + storyboard_height: Int32, + storyboard_count: Int32) + + return items if !storyboards + + url = URI.parse(storyboards.shift) + params = HTTP::Params.parse(url.query || "") + + storyboards.each_with_index do |sb, i| + width, height, count, storyboard_width, storyboard_height, interval, _, sigh = sb.split("#") + params["sigh"] = sigh + url.query = params.to_s + + width = width.to_i + height = height.to_i + count = count.to_i + interval = interval.to_i + storyboard_width = storyboard_width.to_i + storyboard_height = storyboard_height.to_i + storyboard_count = (count / (storyboard_width * storyboard_height)).ceil.to_i + + items << { + url: url.to_s.sub("$L", i).sub("$N", "M$M"), + width: width, + height: height, + count: count, + interval: interval, + storyboard_width: storyboard_width, + storyboard_height: storyboard_height, + storyboard_count: storyboard_count, + } + end + + items + end + end +end From 8327862697774cd8076335fe2002875dd8c5a84a Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sun, 8 Oct 2023 20:09:38 +0200 Subject: [PATCH 112/136] Storyboards: Use replace the NamedTuple by a struct --- src/invidious/jsonify/api_v1/video_json.cr | 18 +++---- src/invidious/routes/api/v1/videos.cr | 19 +++---- src/invidious/videos/storyboard.cr | 61 ++++++++++++---------- 3 files changed, 53 insertions(+), 45 deletions(-) diff --git a/src/invidious/jsonify/api_v1/video_json.cr b/src/invidious/jsonify/api_v1/video_json.cr index 59714828..44a34b18 100644 --- a/src/invidious/jsonify/api_v1/video_json.cr +++ b/src/invidious/jsonify/api_v1/video_json.cr @@ -273,15 +273,15 @@ module Invidious::JSONify::APIv1 json.array do storyboards.each do |storyboard| json.object do - json.field "url", "/api/v1/storyboards/#{id}?width=#{storyboard[:width]}&height=#{storyboard[:height]}" - json.field "templateUrl", storyboard[:url] - json.field "width", storyboard[:width] - json.field "height", storyboard[:height] - json.field "count", storyboard[:count] - json.field "interval", storyboard[:interval] - json.field "storyboardWidth", storyboard[:storyboard_width] - json.field "storyboardHeight", storyboard[:storyboard_height] - json.field "storyboardCount", storyboard[:storyboard_count] + json.field "url", "/api/v1/storyboards/#{id}?width=#{storyboard.width}&height=#{storyboard.height}" + json.field "templateUrl", storyboard.url + json.field "width", storyboard.width + json.field "height", storyboard.height + json.field "count", storyboard.count + json.field "interval", storyboard.interval + json.field "storyboardWidth", storyboard.storyboard_width + json.field "storyboardHeight", storyboard.storyboard_height + json.field "storyboardCount", storyboard.storyboard_count end end end diff --git a/src/invidious/routes/api/v1/videos.cr b/src/invidious/routes/api/v1/videos.cr index 42282f44..78f91a2e 100644 --- a/src/invidious/routes/api/v1/videos.cr +++ b/src/invidious/routes/api/v1/videos.cr @@ -205,7 +205,7 @@ module Invidious::Routes::API::V1::Videos env.response.content_type = "text/vtt" - storyboard = storyboards.select { |sb| width == "#{sb[:width]}" || height == "#{sb[:height]}" } + storyboard = storyboards.select { |sb| width == "#{sb.width}" || height == "#{sb.height}" } if storyboard.empty? haltf env, 404 @@ -215,21 +215,22 @@ module Invidious::Routes::API::V1::Videos WebVTT.build do |vtt| start_time = 0.milliseconds - end_time = storyboard[:interval].milliseconds + end_time = storyboard.interval.milliseconds - storyboard[:storyboard_count].times do |i| - url = storyboard[:url] + storyboard.storyboard_count.times do |i| + url = storyboard.url 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}" - storyboard[:storyboard_height].times do |j| - storyboard[:storyboard_width].times do |k| - current_cue_url = "#{url}#xywh=#{storyboard[:width] * k},#{storyboard[:height] * j},#{storyboard[:width] - 2},#{storyboard[:height]}" + storyboard.storyboard_height.times do |j| + storyboard.storyboard_width.times do |k| + current_cue_url = "#{url}#xywh=#{storyboard.width * k},#{storyboard.height * j},#{storyboard.width - 2},#{storyboard.height}" vtt.cue(start_time, end_time, current_cue_url) - start_time += storyboard[:interval].milliseconds - end_time += storyboard[:interval].milliseconds + start_time += storyboard.interval.milliseconds + end_time += storyboard.interval.milliseconds end end end diff --git a/src/invidious/videos/storyboard.cr b/src/invidious/videos/storyboard.cr index b4302d88..797fba12 100644 --- a/src/invidious/videos/storyboard.cr +++ b/src/invidious/videos/storyboard.cr @@ -3,6 +3,21 @@ require "http/params" module Invidious::Videos struct Storyboard + getter url : String + getter width : Int32 + getter height : Int32 + getter count : Int32 + getter interval : Int32 + getter storyboard_width : Int32 + getter storyboard_height : Int32 + getter storyboard_count : Int32 + + def initialize( + *, @url, @width, @height, @count, @interval, + @storyboard_width, @storyboard_height, @storyboard_count + ) + end + # Parse the JSON structure from Youtube def self.from_yt_json(container : JSON::Any) storyboards = container.dig?("playerStoryboardSpecRenderer", "spec") @@ -10,28 +25,20 @@ module Invidious::Videos if !storyboards if storyboard = container.dig?("playerLiveStoryboardSpecRenderer", "spec").try &.as_s - return [{ - url: storyboard.split("#")[0], - width: 106, - height: 60, - count: -1, - interval: 5000, - storyboard_width: 3, + return [Storyboard.new( + url: storyboard.split("#")[0], + width: 106, + height: 60, + count: -1, + interval: 5000, + storyboard_width: 3, storyboard_height: 3, - storyboard_count: -1, - }] + storyboard_count: -1, + )] end end - items = [] of NamedTuple( - url: String, - width: Int32, - height: Int32, - count: Int32, - interval: Int32, - storyboard_width: Int32, - storyboard_height: Int32, - storyboard_count: Int32) + items = [] of Storyboard return items if !storyboards @@ -51,16 +58,16 @@ module Invidious::Videos storyboard_height = storyboard_height.to_i storyboard_count = (count / (storyboard_width * storyboard_height)).ceil.to_i - items << { - url: url.to_s.sub("$L", i).sub("$N", "M$M"), - width: width, - height: height, - count: count, - interval: interval, - storyboard_width: storyboard_width, + items << Storyboard.new( + url: url.to_s.sub("$L", i).sub("$N", "M$M"), + width: width, + height: height, + count: count, + interval: interval, + storyboard_width: storyboard_width, storyboard_height: storyboard_height, - storyboard_count: storyboard_count, - } + storyboard_count: storyboard_count + ) end items From da3d58f03c9b1617f96f4caf1e348a35105dd79c Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sun, 8 Oct 2023 20:29:41 +0200 Subject: [PATCH 113/136] Storyboards: Cleanup and document code --- src/invidious/jsonify/api_v1/video_json.cr | 20 ++-- src/invidious/routes/api/v1/videos.cr | 52 +++++----- src/invidious/videos/storyboard.cr | 114 ++++++++++++++------- 3 files changed, 113 insertions(+), 73 deletions(-) diff --git a/src/invidious/jsonify/api_v1/video_json.cr b/src/invidious/jsonify/api_v1/video_json.cr index 44a34b18..4d12a072 100644 --- a/src/invidious/jsonify/api_v1/video_json.cr +++ b/src/invidious/jsonify/api_v1/video_json.cr @@ -271,17 +271,17 @@ module Invidious::JSONify::APIv1 def storyboards(json, id, storyboards) json.array do - storyboards.each do |storyboard| + storyboards.each do |sb| json.object do - json.field "url", "/api/v1/storyboards/#{id}?width=#{storyboard.width}&height=#{storyboard.height}" - json.field "templateUrl", storyboard.url - json.field "width", storyboard.width - json.field "height", storyboard.height - json.field "count", storyboard.count - json.field "interval", storyboard.interval - json.field "storyboardWidth", storyboard.storyboard_width - json.field "storyboardHeight", storyboard.storyboard_height - json.field "storyboardCount", storyboard.storyboard_count + json.field "url", "/api/v1/storyboards/#{id}?width=#{sb.width}&height=#{sb.height}" + json.field "templateUrl", sb.url.to_s + json.field "width", sb.width + json.field "height", sb.height + json.field "count", sb.count + json.field "interval", sb.interval + json.field "storyboardWidth", sb.columns + json.field "storyboardHeight", sb.rows + json.field "storyboardCount", sb.images_count end end end diff --git a/src/invidious/routes/api/v1/videos.cr b/src/invidious/routes/api/v1/videos.cr index 78f91a2e..fb083934 100644 --- a/src/invidious/routes/api/v1/videos.cr +++ b/src/invidious/routes/api/v1/videos.cr @@ -187,15 +187,14 @@ module Invidious::Routes::API::V1::Videos haltf env, 500 end - storyboards = video.storyboards - width = env.params.query["width"]? - height = env.params.query["height"]? + width = env.params.query["width"]?.try &.to_i + height = env.params.query["height"]?.try &.to_i if !width && !height response = JSON.build do |json| json.object do json.field "storyboards" do - Invidious::JSONify::APIv1.storyboards(json, id, storyboards) + Invidious::JSONify::APIv1.storyboards(json, id, video.storyboards) end end end @@ -205,32 +204,37 @@ module Invidious::Routes::API::V1::Videos env.response.content_type = "text/vtt" - storyboard = storyboards.select { |sb| width == "#{sb.width}" || height == "#{sb.height}" } + # Select a storyboard matching the user's provided width/height + storyboard = video.storyboards.select { |x| x.width == width || x.height == height } + haltf env, 404 if storyboard.empty? - if storyboard.empty? - haltf env, 404 - else - storyboard = storyboard[0] - end + # Alias variable, to make the code below esaier to read + sb = storyboard[0] - WebVTT.build do |vtt| - start_time = 0.milliseconds - end_time = storyboard.interval.milliseconds + # Some base URL segments that we'll use to craft the final URLs + work_url = sb.proxied_url.dup + template_path = sb.proxied_url.path - storyboard.storyboard_count.times do |i| - url = storyboard.url - authority = /(i\d?).ytimg.com/.match!(url)[1]? + # Initialize cue timing variables + time_delta = sb.interval.milliseconds + start_time = 0.milliseconds + end_time = time_delta - 1.milliseconds - url = url.gsub("$M", i).gsub(%r(https://i\d?.ytimg.com/sb/), "") - url = "#{HOST_URL}/sb/#{authority}/#{url}" + # Build a VTT file for VideoJS-vtt plugin + return WebVTT.build do |vtt| + sb.images_count.times do |i| + # Replace the variable component part of the path + work_url.path = template_path.sub("$M", i) - storyboard.storyboard_height.times do |j| - storyboard.storyboard_width.times do |k| - current_cue_url = "#{url}#xywh=#{storyboard.width * k},#{storyboard.height * j},#{storyboard.width - 2},#{storyboard.height}" - vtt.cue(start_time, end_time, current_cue_url) + sb.rows.times do |j| + sb.columns.times do |k| + # The URL fragment represents the offset of the thumbnail inside the storyboard image + work_url.fragment = "xywh=#{sb.width * k},#{sb.height * j},#{sb.width - 2},#{sb.height}" - start_time += storyboard.interval.milliseconds - end_time += storyboard.interval.milliseconds + vtt.cue(start_time, end_time, work_url.to_s) + + start_time += time_delta + end_time += time_delta end end end diff --git a/src/invidious/videos/storyboard.cr b/src/invidious/videos/storyboard.cr index 797fba12..f6df187f 100644 --- a/src/invidious/videos/storyboard.cr +++ b/src/invidious/videos/storyboard.cr @@ -3,74 +3,110 @@ require "http/params" module Invidious::Videos struct Storyboard - getter url : String + # Template URL + getter url : URI + getter proxied_url : URI + + # Thumbnail parameters getter width : Int32 getter height : Int32 getter count : Int32 getter interval : Int32 - getter storyboard_width : Int32 - getter storyboard_height : Int32 - getter storyboard_count : Int32 + + # Image (storyboard) parameters + getter rows : Int32 + getter columns : Int32 + getter images_count : Int32 def initialize( *, @url, @width, @height, @count, @interval, - @storyboard_width, @storyboard_height, @storyboard_count + @rows, @columns, @images_count ) + authority = /(i\d?).ytimg.com/.match(@url.host.not_nil!).not_nil![1]? + + @proxied_url = URI.parse(HOST_URL) + @proxied_url.path = "/sb/#{authority}#{@url.path}" + @proxied_url.query = @url.query end # Parse the JSON structure from Youtube - def self.from_yt_json(container : JSON::Any) + def self.from_yt_json(container : JSON::Any) : Array(Storyboard) + # Livestream storyboards are a bit different + # TODO: document exactly how + if storyboard = container.dig?("playerLiveStoryboardSpecRenderer", "spec").try &.as_s + return [Storyboard.new( + url: URI.parse(storyboard.split("#")[0]), + width: 106, + height: 60, + count: -1, + interval: 5000, + rows: 3, + columns: 3, + images_count: -1 + )] + end + + # Split the storyboard string into chunks + # + # General format (whitespaces added for legibility): + # https://i.ytimg.com/sb//storyboard3_L$L/$N.jpg?sqp= + # | 48 # 27 # 100 # 10 # 10 # 0 # default # rs$ + # | 80 # 45 # 95 # 10 # 10 # 10000 # M$M # rs$ + # | 160 # 90 # 95 # 5 # 5 # 10000 # M$M # rs$ + # storyboards = container.dig?("playerStoryboardSpecRenderer", "spec") .try &.as_s.split("|") - if !storyboards - if storyboard = container.dig?("playerLiveStoryboardSpecRenderer", "spec").try &.as_s - return [Storyboard.new( - url: storyboard.split("#")[0], - width: 106, - height: 60, - count: -1, - interval: 5000, - storyboard_width: 3, - storyboard_height: 3, - storyboard_count: -1, - )] - end - end - - items = [] of Storyboard - - return items if !storyboards + return [] of Storyboard if !storyboards + # The base URL is the first chunk url = URI.parse(storyboards.shift) - params = HTTP::Params.parse(url.query || "") + params = url.query_params - storyboards.each_with_index do |sb, i| - width, height, count, storyboard_width, storyboard_height, interval, _, sigh = sb.split("#") - params["sigh"] = sigh - url.query = params.to_s + return storyboards.map_with_index do |sb, i| + # Separate the different storyboard parameters: + # width/height: respective dimensions, in pixels, of a single thumbnail + # count: how many thumbnails are displayed across the full video + # columns/rows: maximum amount of thumbnails that can be stuffed in a + # single image, horizontally and vertically. + # interval: interval between two thumbnails, in milliseconds + # sigh: URL cryptographic signature + width, height, count, columns, rows, interval, _, sigh = sb.split("#") width = width.to_i height = height.to_i count = count.to_i interval = interval.to_i - storyboard_width = storyboard_width.to_i - storyboard_height = storyboard_height.to_i - storyboard_count = (count / (storyboard_width * storyboard_height)).ceil.to_i + columns = columns.to_i + rows = rows.to_i - items << Storyboard.new( - url: url.to_s.sub("$L", i).sub("$N", "M$M"), + # Add the signature to the URL + params["sigh"] = sigh + url.query = params.to_s + + # Replace the template parts with what we have + url.path = url.path.sub("$L", i).sub("$N", "M$M") + + # This value represents the maximum amount of thumbnails that can fit + # in a single image. The last image (or the only one for short videos) + # will contain less thumbnails than that. + thumbnails_per_image = columns * rows + + # This value represents the total amount of storyboards required to + # hold all of the thumbnails. It can't be less than 1. + images_count = (count / thumbnails_per_image).ceil.to_i + + Storyboard.new( + url: url, width: width, height: height, count: count, interval: interval, - storyboard_width: storyboard_width, - storyboard_height: storyboard_height, - storyboard_count: storyboard_count + rows: rows, + columns: columns, + images_count: images_count, ) end - - items end end end From 7b50388eafcd458221f3deec03bf5a0829244529 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sun, 8 Oct 2023 20:36:32 +0200 Subject: [PATCH 114/136] Storyboards: Fix broken first storyboard --- src/invidious/videos.cr | 2 +- src/invidious/videos/storyboard.cr | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index 73321909..28cbb311 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -178,7 +178,7 @@ struct Video def storyboards container = info.dig?("storyboards") || JSON::Any.new("{}") - return IV::Videos::Storyboard.from_yt_json(container) + return IV::Videos::Storyboard.from_yt_json(container, self.length_seconds) end def paid diff --git a/src/invidious/videos/storyboard.cr b/src/invidious/videos/storyboard.cr index f6df187f..61aafe37 100644 --- a/src/invidious/videos/storyboard.cr +++ b/src/invidious/videos/storyboard.cr @@ -30,7 +30,7 @@ module Invidious::Videos end # Parse the JSON structure from Youtube - def self.from_yt_json(container : JSON::Any) : Array(Storyboard) + def self.from_yt_json(container : JSON::Any, length_seconds : Int32) : Array(Storyboard) # Livestream storyboards are a bit different # TODO: document exactly how if storyboard = container.dig?("playerLiveStoryboardSpecRenderer", "spec").try &.as_s @@ -70,8 +70,9 @@ module Invidious::Videos # columns/rows: maximum amount of thumbnails that can be stuffed in a # single image, horizontally and vertically. # interval: interval between two thumbnails, in milliseconds + # name: storyboard filename. Usually "M$M" or "default" # sigh: URL cryptographic signature - width, height, count, columns, rows, interval, _, sigh = sb.split("#") + width, height, count, columns, rows, interval, name, sigh = sb.split("#") width = width.to_i height = height.to_i @@ -85,7 +86,7 @@ module Invidious::Videos url.query = params.to_s # Replace the template parts with what we have - url.path = url.path.sub("$L", i).sub("$N", "M$M") + url.path = url.path.sub("$L", i).sub("$N", name) # This value represents the maximum amount of thumbnails that can fit # in a single image. The last image (or the only one for short videos) @@ -96,6 +97,12 @@ module Invidious::Videos # hold all of the thumbnails. It can't be less than 1. images_count = (count / thumbnails_per_image).ceil.to_i + # Compute the interval when needed (in general, that's only required + # for the first "default" storyboard). + if interval == 0 + interval = ((length_seconds / count) * 1_000).to_i + end + Storyboard.new( url: url, width: width, From a335bc0814d3253852ed5b5cf58b75d9f7b6cd70 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Fri, 20 Oct 2023 23:37:12 +0200 Subject: [PATCH 115/136] Storyboards: Fix some small logic mistakes --- src/invidious/videos/storyboard.cr | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/invidious/videos/storyboard.cr b/src/invidious/videos/storyboard.cr index 61aafe37..35012663 100644 --- a/src/invidious/videos/storyboard.cr +++ b/src/invidious/videos/storyboard.cr @@ -25,7 +25,7 @@ module Invidious::Videos authority = /(i\d?).ytimg.com/.match(@url.host.not_nil!).not_nil![1]? @proxied_url = URI.parse(HOST_URL) - @proxied_url.path = "/sb/#{authority}#{@url.path}" + @proxied_url.path = "/sb/#{authority}/#{@url.path.lchop("/sb/")}" @proxied_url.query = @url.query end @@ -60,8 +60,7 @@ module Invidious::Videos return [] of Storyboard if !storyboards # The base URL is the first chunk - url = URI.parse(storyboards.shift) - params = url.query_params + base_url = URI.parse(storyboards.shift) return storyboards.map_with_index do |sb, i| # Separate the different storyboard parameters: @@ -81,9 +80,13 @@ module Invidious::Videos columns = columns.to_i rows = rows.to_i + # Copy base URL object, so that we can modify it + url = base_url.dup + # Add the signature to the URL + params = url.query_params params["sigh"] = sigh - url.query = params.to_s + url.query_params = params # Replace the template parts with what we have url.path = url.path.sub("$L", i).sub("$N", name) From 5b05f3bd147c6cf9421587565dea2b11640f1206 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Fri, 16 Aug 2024 11:28:35 +0200 Subject: [PATCH 116/136] Storyboards: Workarounds for videojs-vtt-thumbnails The workarounds are as follow: * Unescape HTML entities * Always use 0:00:00.000 for cue start/end --- src/invidious/routes/api/v1/videos.cr | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/invidious/routes/api/v1/videos.cr b/src/invidious/routes/api/v1/videos.cr index fb083934..ab03df01 100644 --- a/src/invidious/routes/api/v1/videos.cr +++ b/src/invidious/routes/api/v1/videos.cr @@ -1,3 +1,5 @@ +require "html" + module Invidious::Routes::API::V1::Videos def self.videos(env) locale = env.get("preferences").as(Preferences).locale @@ -216,12 +218,14 @@ module Invidious::Routes::API::V1::Videos template_path = sb.proxied_url.path # Initialize cue timing variables + # NOTE: videojs-vtt-thumbnails gets lost when the start and end times are not 0:00:000.000 + # TODO: Use proper end time when videojs-vtt-thumbnails is fixed time_delta = sb.interval.milliseconds start_time = 0.milliseconds - end_time = time_delta - 1.milliseconds + end_time = 0.milliseconds # time_delta - 1.milliseconds # Build a VTT file for VideoJS-vtt plugin - return WebVTT.build do |vtt| + vtt_file = WebVTT.build do |vtt| sb.images_count.times do |i| # Replace the variable component part of the path work_url.path = template_path.sub("$M", i) @@ -233,12 +237,18 @@ module Invidious::Routes::API::V1::Videos vtt.cue(start_time, end_time, work_url.to_s) - start_time += time_delta - end_time += time_delta + # TODO: uncomment these when videojs-vtt-thumbnails is fixed + # start_time += time_delta + # end_time += time_delta end end end end + + # videojs-vtt-thumbnails is not compliant to the VTT specification, it + # doesn't unescape the HTML entities, so we have to do it here: + # TODO: remove this when we migrate to VideoJS 8 + return HTML.unescape(vtt_file) end def self.annotations(env) From b795bdf2a4a50fc899fde9dc7b42b845a4588bfc Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Fri, 16 Aug 2024 12:10:22 +0200 Subject: [PATCH 117/136] HTML: Sort playlists alphabetically in watch page drop down --- src/invidious/database/playlists.cr | 1 + 1 file changed, 1 insertion(+) diff --git a/src/invidious/database/playlists.cr b/src/invidious/database/playlists.cr index c6754a1e..08aa719a 100644 --- a/src/invidious/database/playlists.cr +++ b/src/invidious/database/playlists.cr @@ -140,6 +140,7 @@ module Invidious::Database::Playlists request = <<-SQL SELECT id,title FROM playlists WHERE author = $1 AND id LIKE 'IV%' + ORDER BY title SQL PG_DB.query_all(request, email, as: {String, String}) From 764965c441a789e0be417648716f575067d9201e Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 17 Aug 2024 12:20:53 +0200 Subject: [PATCH 118/136] Storyboards: Fix lint error --- src/invidious/videos/storyboard.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/videos/storyboard.cr b/src/invidious/videos/storyboard.cr index 35012663..a72c2f55 100644 --- a/src/invidious/videos/storyboard.cr +++ b/src/invidious/videos/storyboard.cr @@ -22,7 +22,7 @@ module Invidious::Videos *, @url, @width, @height, @count, @interval, @rows, @columns, @images_count ) - authority = /(i\d?).ytimg.com/.match(@url.host.not_nil!).not_nil![1]? + authority = /(i\d?).ytimg.com/.match!(@url.host.not_nil!)[1]? @proxied_url = URI.parse(HOST_URL) @proxied_url.path = "/sb/#{authority}/#{@url.path.lchop("/sb/")}" From eb0f651812d7d01c038f5a052bf30fc8e26b877f Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sun, 1 Oct 2023 19:39:53 +0200 Subject: [PATCH 119/136] Add a youtube URL sanitizer --- src/invidious/yt_backend/url_sanitizer.cr | 121 ++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 src/invidious/yt_backend/url_sanitizer.cr diff --git a/src/invidious/yt_backend/url_sanitizer.cr b/src/invidious/yt_backend/url_sanitizer.cr new file mode 100644 index 00000000..02bf77bf --- /dev/null +++ b/src/invidious/yt_backend/url_sanitizer.cr @@ -0,0 +1,121 @@ +require "uri" + +module UrlSanitizer + extend self + + ALLOWED_QUERY_PARAMS = { + channel: ["u", "user", "lb"], + playlist: ["list"], + search: ["q", "search_query", "sp"], + watch: [ + "v", # Video ID + "list", "index", # Playlist-related + "playlist", # Unnamed playlist (id,id,id,...) (embed-only?) + "t", "time_continue", "start", "end", # Timestamp + "lc", # Highlighted comment (watch page only) + ], + } + + # Returns wether the given string is an ASCII word. This is the same as + # running the following regex in US-ASCII locale: /^[\w-]+$/ + private def ascii_word?(str : String) : Bool + if str.bytesize == str.size + str.each_byte do |byte| + next if 'a'.ord <= byte <= 'z'.ord + next if 'A'.ord <= byte <= 'Z'.ord + next if '0'.ord <= byte <= '9'.ord + next if byte == '-'.ord || byte == '_'.ord + + return false + end + + return true + else + return false + end + end + + # Return which kind of parameters are allowed based on the + # first path component (breadcrumb 0). + private def determine_allowed(path_root : String) + case path_root + when "watch", "w", "v", "embed", "e", "shorts", "clip" + return :watch + when .starts_with?("@"), "c", "channel", "user", "profile", "attribution_link" + return :channel + when "playlist", "mix" + return :playlist + when "results", "search" + return :search + else # hashtag, post, trending, brand URLs, etc.. + return nil + end + end + + # Create a new URI::Param containing only the allowed parameters + private def copy_params(unsafe_params : URI::Params, allowed_type) : URI::Params + new_params = URI::Params.new + + ALLOWED_QUERY_PARAMS[allowed_type].each do |name| + if unsafe_params[name]? + # Only copy the last parameter, in case there is more than one + new_params[name] = unsafe_params.fetch_all(name)[-1] + end + end + + return new_params + end + + # Transform any user-supplied youtube URL into something we can trust + # and use across the code. + def process(str : String) : URI + # Because URI follows RFC3986 specifications, URL without a scheme + # will be parsed as a relative path. So we have to add a scheme ourselves. + str = "https://#{str}" if !str.starts_with?(/https?:\/\//) + + unsafe_uri = URI.parse(str) + new_uri = URI.new(path: "/") + + # Redirect to homepage for bogus URLs + return new_uri if (unsafe_uri.host.nil? || unsafe_uri.path.nil?) + + breadcrumbs = unsafe_uri.path + .split('/', remove_empty: true) + .compact_map do |bc| + # Exclude attempts at path trasversal + next if bc == "." || bc == ".." + + # Non-alnum characters are unlikely in a genuine URL + next if !ascii_word?(bc) + + bc + end + + # If nothing remains, it's either a legit URL to the homepage + # (who does that!?) or because we filtered some junk earlier. + return new_uri if breadcrumbs.empty? + + # Replace the original query parameters with the sanitized ones + case unsafe_uri.host.not_nil! + when .ends_with?("youtube.com") + # Use our sanitized path (not forgetting the leading '/') + new_uri.path = "/#{breadcrumbs.join('/')}" + + # Then determine which params are allowed, and copy them over + if allowed = determine_allowed(breadcrumbs[0]) + new_uri.query_params = copy_params(unsafe_uri.query_params, allowed) + end + when "youtu.be" + # Always redirect to the watch page + new_uri.path = "/watch" + + new_params = copy_params(unsafe_uri.query_params, :watch) + new_params["id"] = breadcrumbs[0] + + new_uri.query_params = new_params + end + + new_uri.host = nil # Safety measure + return new_uri + end +end From 4c0b5c314d68ea45e69de9673f0bf43bedf3acc4 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Thu, 5 Oct 2023 23:01:44 +0200 Subject: [PATCH 120/136] Search: Add support for youtu.be and youtube.com URLs --- src/invidious/routes/search.cr | 6 ++++++ src/invidious/search/query.cr | 27 +++++++++++++++++++++------ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/invidious/routes/search.cr b/src/invidious/routes/search.cr index 5be33533..85aa1c7e 100644 --- a/src/invidious/routes/search.cr +++ b/src/invidious/routes/search.cr @@ -51,6 +51,12 @@ module Invidious::Routes::Search else user = env.get? "user" + # An URL was copy/pasted in the search box. + # Redirect the user to the appropriate page. + if query.is_url? + return env.redirect UrlSanitizer.process(query.text).to_s + end + begin items = query.process rescue ex : ChannelSearchException diff --git a/src/invidious/search/query.cr b/src/invidious/search/query.cr index e38845d9..f87c243e 100644 --- a/src/invidious/search/query.cr +++ b/src/invidious/search/query.cr @@ -48,11 +48,12 @@ module Invidious::Search ) # Get the raw search query string (common to all search types). In # Regular search mode, also look for the `search_query` URL parameter - if @type.regular? - @raw_query = params["q"]? || params["search_query"]? || "" - else - @raw_query = params["q"]? || "" - end + _raw_query = params["q"]? + _raw_query ||= params["search_query"]? if @type.regular? + _raw_query ||= "" + + # Remove surrounding whitespaces. Mostly useful for copy/pasted URLs. + @raw_query = _raw_query.strip # Get the page number (also common to all search types) @page = params["page"]?.try &.to_i? || 1 @@ -85,7 +86,7 @@ module Invidious::Search @filters = Filters.from_iv_params(params) @channel = params["channel"]? || "" - if @filters.default? && @raw_query.includes?(':') + if @filters.default? && @raw_query.index(/\w:\w/) # Parse legacy filters from query @filters, @channel, @query, subs = Filters.from_legacy_filters(@raw_query) else @@ -136,5 +137,19 @@ module Invidious::Search return params end + + # Checks if the query is a standalone URL + def is_url? : Bool + # Only supported in regular search mode + return false if !@type.regular? + + # If filters are present, that's a regular search + return false if !@filters.default? + + # Simple heuristics: domain name + return @raw_query.starts_with?( + /(https?:\/\/)?(www\.)?(m\.)?youtu(\.be|be\.com)\// + ) + end end end From 31a80420ec9f4dbd61a7145044f5e1797d4e0dd0 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Tue, 13 Feb 2024 21:46:12 +0100 Subject: [PATCH 121/136] Search: Add URL search inhibition logic --- src/invidious/search/query.cr | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/invidious/search/query.cr b/src/invidious/search/query.cr index f87c243e..b3db0f63 100644 --- a/src/invidious/search/query.cr +++ b/src/invidious/search/query.cr @@ -20,6 +20,9 @@ module Invidious::Search property region : String? property channel : String = "" + # Flag that indicates if the smart search features have been disabled. + @inhibit_ssf : Bool = false + # Return true if @raw_query is either `nil` or empty private def empty_raw_query? return @raw_query.empty? @@ -55,6 +58,13 @@ module Invidious::Search # Remove surrounding whitespaces. Mostly useful for copy/pasted URLs. @raw_query = _raw_query.strip + # Check for smart features (ex: URL search) inhibitor (exclamation mark). + # If inhibitor is present, remove it. + if @raw_query.starts_with?('!') + @inhibit_ssf = true + @raw_query = @raw_query[1..] + end + # Get the page number (also common to all search types) @page = params["page"]?.try &.to_i? || 1 @@ -140,6 +150,9 @@ module Invidious::Search # Checks if the query is a standalone URL def is_url? : Bool + # If the smart features have been inhibited, don't go further. + return false if @inhibit_ssf + # Only supported in regular search mode return false if !@type.regular? From 78c5ba93c7f4eecf7aae623079c0c77f78670b67 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 17 Feb 2024 14:27:25 +0100 Subject: [PATCH 122/136] Misc: Clean some code in UrlSanitizer --- src/invidious/yt_backend/url_sanitizer.cr | 30 +++++++++++------------ 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/invidious/yt_backend/url_sanitizer.cr b/src/invidious/yt_backend/url_sanitizer.cr index 02bf77bf..725382ee 100644 --- a/src/invidious/yt_backend/url_sanitizer.cr +++ b/src/invidious/yt_backend/url_sanitizer.cr @@ -16,23 +16,21 @@ module UrlSanitizer ], } - # Returns wether the given string is an ASCII word. This is the same as + # Returns whether the given string is an ASCII word. This is the same as # running the following regex in US-ASCII locale: /^[\w-]+$/ private def ascii_word?(str : String) : Bool - if str.bytesize == str.size - str.each_byte do |byte| - next if 'a'.ord <= byte <= 'z'.ord - next if 'A'.ord <= byte <= 'Z'.ord - next if '0'.ord <= byte <= '9'.ord - next if byte == '-'.ord || byte == '_'.ord + return false if str.bytesize != str.size - return false - end + str.each_byte do |byte| + next if 'a'.ord <= byte <= 'z'.ord + next if 'A'.ord <= byte <= 'Z'.ord + next if '0'.ord <= byte <= '9'.ord + next if byte == '-'.ord || byte == '_'.ord - return true - else return false end + + return true end # Return which kind of parameters are allowed based on the @@ -74,12 +72,15 @@ module UrlSanitizer str = "https://#{str}" if !str.starts_with?(/https?:\/\//) unsafe_uri = URI.parse(str) + unsafe_host = unsafe_uri.host + unsafe_path = unsafe_uri.path + new_uri = URI.new(path: "/") # Redirect to homepage for bogus URLs - return new_uri if (unsafe_uri.host.nil? || unsafe_uri.path.nil?) + return new_uri if (unsafe_host.nil? || unsafe_path.nil?) - breadcrumbs = unsafe_uri.path + breadcrumbs = unsafe_path .split('/', remove_empty: true) .compact_map do |bc| # Exclude attempts at path trasversal @@ -96,7 +97,7 @@ module UrlSanitizer return new_uri if breadcrumbs.empty? # Replace the original query parameters with the sanitized ones - case unsafe_uri.host.not_nil! + case unsafe_host when .ends_with?("youtube.com") # Use our sanitized path (not forgetting the leading '/') new_uri.path = "/#{breadcrumbs.join('/')}" @@ -115,7 +116,6 @@ module UrlSanitizer new_uri.query_params = new_params end - new_uri.host = nil # Safety measure return new_uri end end From 85deea5aca4877507bb8850e5e3e168d968328ad Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sat, 27 Apr 2024 23:21:27 +0200 Subject: [PATCH 123/136] Search: Change smart search inhibitor to a backslash --- src/invidious/search/query.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/invidious/search/query.cr b/src/invidious/search/query.cr index b3db0f63..a93bb3f9 100644 --- a/src/invidious/search/query.cr +++ b/src/invidious/search/query.cr @@ -58,9 +58,9 @@ module Invidious::Search # Remove surrounding whitespaces. Mostly useful for copy/pasted URLs. @raw_query = _raw_query.strip - # Check for smart features (ex: URL search) inhibitor (exclamation mark). + # Check for smart features (ex: URL search) inhibitor (backslash). # If inhibitor is present, remove it. - if @raw_query.starts_with?('!') + if @raw_query.starts_with?('\\') @inhibit_ssf = true @raw_query = @raw_query[1..] end From c606465708720c953c37032624ff31e5e9d841ab Mon Sep 17 00:00:00 2001 From: Colin Leroy-Mira Date: Mon, 19 Aug 2024 09:34:51 +0200 Subject: [PATCH 124/136] Proxify formatStreams URLs too --- src/invidious/jsonify/api_v1/video_json.cr | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/invidious/jsonify/api_v1/video_json.cr b/src/invidious/jsonify/api_v1/video_json.cr index 59714828..e4379601 100644 --- a/src/invidious/jsonify/api_v1/video_json.cr +++ b/src/invidious/jsonify/api_v1/video_json.cr @@ -162,7 +162,13 @@ module Invidious::JSONify::APIv1 json.array do video.fmt_stream.each do |fmt| json.object do - json.field "url", fmt["url"] + if proxy + json.field "url", Invidious::HttpServer::Utils.proxy_video_url( + fmt["url"].to_s, absolute: true + ) + else + json.field "url", fmt["url"] + end json.field "itag", fmt["itag"].as_i.to_s json.field "type", fmt["mimeType"] json.field "quality", fmt["quality"] From 22b35c453ede48e36db1657c5b8e879f3cc70a56 Mon Sep 17 00:00:00 2001 From: syeopite Date: Thu, 25 Jul 2024 20:12:17 -0700 Subject: [PATCH 125/136] Ameba: Fix Style/WhileTrue --- src/invidious/routes/video_playback.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/routes/video_playback.cr b/src/invidious/routes/video_playback.cr index ec18f3b8..24693662 100644 --- a/src/invidious/routes/video_playback.cr +++ b/src/invidious/routes/video_playback.cr @@ -131,7 +131,7 @@ module Invidious::Routes::VideoPlayback end # TODO: Record bytes written so we can restart after a chunk fails - while true + loop do if !range_end && content_length range_end = content_length end From f66068976e5f077d363769055b7533cd0f85d6d0 Mon Sep 17 00:00:00 2001 From: syeopite Date: Fri, 26 Jul 2024 19:19:31 -0700 Subject: [PATCH 126/136] Ameba: Fix Naming/PredicateName --- src/invidious/helpers/serialized_yt_data.cr | 4 ++-- src/invidious/jsonify/api_v1/video_json.cr | 2 +- src/invidious/user/imports.cr | 4 ++-- src/invidious/videos.cr | 20 ++++++++++++++++++-- src/invidious/views/watch.ecr | 2 +- 5 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/invidious/helpers/serialized_yt_data.cr b/src/invidious/helpers/serialized_yt_data.cr index 31a3cf44..463d5557 100644 --- a/src/invidious/helpers/serialized_yt_data.cr +++ b/src/invidious/helpers/serialized_yt_data.cr @@ -90,7 +90,7 @@ struct SearchVideo json.field "lengthSeconds", self.length_seconds json.field "liveNow", self.live_now json.field "premium", self.premium - json.field "isUpcoming", self.is_upcoming + json.field "isUpcoming", self.upcoming? if self.premiere_timestamp json.field "premiereTimestamp", self.premiere_timestamp.try &.to_unix @@ -109,7 +109,7 @@ struct SearchVideo to_json(nil, json) end - def is_upcoming + def upcoming? premiere_timestamp ? true : false end end diff --git a/src/invidious/jsonify/api_v1/video_json.cr b/src/invidious/jsonify/api_v1/video_json.cr index 59714828..2d41ed3b 100644 --- a/src/invidious/jsonify/api_v1/video_json.cr +++ b/src/invidious/jsonify/api_v1/video_json.cr @@ -63,7 +63,7 @@ module Invidious::JSONify::APIv1 json.field "isListed", video.is_listed json.field "liveNow", video.live_now json.field "isPostLiveDvr", video.post_live_dvr - json.field "isUpcoming", video.is_upcoming + json.field "isUpcoming", video.upcoming? if video.premiere_timestamp json.field "premiereTimestamp", video.premiere_timestamp.try &.to_unix diff --git a/src/invidious/user/imports.cr b/src/invidious/user/imports.cr index a70434ca..2b5f88f4 100644 --- a/src/invidious/user/imports.cr +++ b/src/invidious/user/imports.cr @@ -161,7 +161,7 @@ struct Invidious::User # Youtube # ------------------- - private def is_opml?(mimetype : String, extension : String) + private def opml?(mimetype : String, extension : String) opml_mimetypes = [ "application/xml", "text/xml", @@ -179,7 +179,7 @@ struct Invidious::User def from_youtube(user : User, body : String, filename : String, type : String) : Bool extension = filename.split(".").last - if is_opml?(type, extension) + if 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}/)[0] diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index 6d0cf9ba..65b07fe8 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -280,7 +280,7 @@ struct Video info["genreUcid"].try &.as_s? ? "/channel/#{info["genreUcid"]}" : nil end - def is_vr : Bool? + def vr? : Bool? return {"EQUIRECTANGULAR", "MESH"}.includes? self.projection_type end @@ -361,6 +361,21 @@ struct Video {% if flag?(:debug_macros) %} {{debug}} {% end %} end + # Macro to generate ? and = accessor methods for attributes in `info` + private macro predicate_bool(method_name, name) + # Return {{name.stringify}} from `info` + def {{method_name.id.underscore}}? : Bool + return info[{{name.stringify}}]?.try &.as_bool || false + end + + # Update {{name.stringify}} into `info` + def {{method_name.id.underscore}}=(value : Bool) + info[{{name.stringify}}] = JSON::Any.new(value) + end + + {% if flag?(:debug_macros) %} {{debug}} {% end %} + end + # Method definitions, using the macros above getset_string author @@ -382,11 +397,12 @@ struct Video getset_i64 likes getset_i64 views + # TODO: Make predicate_bool the default as to adhere to Crystal conventions getset_bool allowRatings getset_bool authorVerified getset_bool isFamilyFriendly getset_bool isListed - getset_bool isUpcoming + predicate_bool upcoming, isUpcoming end def get_video(id, refresh = true, region = nil, force_refresh = false) diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 36679bce..45c58a16 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -62,7 +62,7 @@ we're going to need to do it here in order to allow for translations. "params" => params, "preferences" => preferences, "premiere_timestamp" => video.premiere_timestamp.try &.to_unix, - "vr" => video.is_vr, + "vr" => video.vr?, "projection_type" => video.projection_type, "local_disabled" => CONFIG.disabled?("local"), "support_reddit" => true From d1cd7903882b23eedae6ff28441c1adc40b5be7b Mon Sep 17 00:00:00 2001 From: syeopite Date: Fri, 26 Jul 2024 19:20:06 -0700 Subject: [PATCH 127/136] Ameba: Fix Lint/RedundantStringCoercion --- src/invidious/jsonify/api_v1/video_json.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/jsonify/api_v1/video_json.cr b/src/invidious/jsonify/api_v1/video_json.cr index 2d41ed3b..3625b8f1 100644 --- a/src/invidious/jsonify/api_v1/video_json.cr +++ b/src/invidious/jsonify/api_v1/video_json.cr @@ -109,7 +109,7 @@ module Invidious::JSONify::APIv1 # On livestreams, it's not present, so always fall back to the # current unix timestamp (up to mS precision) for compatibility. last_modified = fmt["lastModified"]? - last_modified ||= "#{Time.utc.to_unix_ms.to_s}000" + last_modified ||= "#{Time.utc.to_unix_ms}000" json.field "lmt", last_modified json.field "projectionType", fmt["projectionType"] From ecbea0b67b7b478597e40b530c0df8cd212e4faf Mon Sep 17 00:00:00 2001 From: syeopite Date: Fri, 26 Jul 2024 19:22:42 -0700 Subject: [PATCH 128/136] Ameba: Fix Lint/ShadowingOuterLocalVar --- src/invidious/routes/api/v1/videos.cr | 4 ++-- src/invidious/user/imports.cr | 2 +- src/invidious/videos/transcript.cr | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/invidious/routes/api/v1/videos.cr b/src/invidious/routes/api/v1/videos.cr index 42282f44..c49a9b7b 100644 --- a/src/invidious/routes/api/v1/videos.cr +++ b/src/invidious/routes/api/v1/videos.cr @@ -116,7 +116,7 @@ module Invidious::Routes::API::V1::Videos else caption_xml = XML.parse(caption_xml) - webvtt = WebVTT.build(settings_field) do |webvtt| + webvtt = WebVTT.build(settings_field) do |builder| caption_nodes = caption_xml.xpath_nodes("//transcript/text") caption_nodes.each_with_index do |node, i| start_time = node["start"].to_f.seconds @@ -136,7 +136,7 @@ module Invidious::Routes::API::V1::Videos text = "#{md["text"]}" end - webvtt.cue(start_time, end_time, text) + builder.cue(start_time, end_time, text) end end end diff --git a/src/invidious/user/imports.cr b/src/invidious/user/imports.cr index 2b5f88f4..533c18d9 100644 --- a/src/invidious/user/imports.cr +++ b/src/invidious/user/imports.cr @@ -115,7 +115,7 @@ struct Invidious::User playlists.each do |item| title = item["title"]?.try &.as_s?.try &.delete("<>") description = item["description"]?.try &.as_s?.try &.delete("\r") - privacy = item["privacy"]?.try &.as_s?.try { |privacy| PlaylistPrivacy.parse? privacy } + privacy = item["privacy"]?.try &.as_s?.try { |raw_pl_privacy_state| PlaylistPrivacy.parse? raw_pl_privacy_state } next if !title next if !description diff --git a/src/invidious/videos/transcript.cr b/src/invidious/videos/transcript.cr index 9cd064c5..4bd9f820 100644 --- a/src/invidious/videos/transcript.cr +++ b/src/invidious/videos/transcript.cr @@ -110,13 +110,13 @@ module Invidious::Videos "Language" => @language_code, } - vtt = WebVTT.build(settings_field) do |vtt| + vtt = WebVTT.build(settings_field) do |builder| @lines.each do |line| # Section headers are excluded from the VTT conversion as to # match the regular captions returned from YouTube as much as possible next if line.is_a? HeadingLine - vtt.cue(line.start_ms, line.end_ms, line.line) + builder.cue(line.start_ms, line.end_ms, line.line) end end From b200ebfb6bc9de169d288c3d816332ea439fbdb6 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Wed, 21 Aug 2024 20:23:45 +0000 Subject: [PATCH 129/136] CSS: Remove extra space in default.css --- assets/css/default.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/css/default.css b/assets/css/default.css index 1445f65f..2cedcf0c 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -282,7 +282,7 @@ div.thumbnail > .bottom-right-overlay { display: flex; } -.searchbar .pure-form fieldset { +.searchbar .pure-form fieldset { padding: 0; flex: 1; } From 21ab5dc6680da3df62feed14c00104754f2479a4 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Thu, 22 Aug 2024 00:29:15 +0200 Subject: [PATCH 130/136] Storyboard: Revert cue timing "fix" --- src/invidious/routes/api/v1/videos.cr | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/invidious/routes/api/v1/videos.cr b/src/invidious/routes/api/v1/videos.cr index ab03df01..c077b85e 100644 --- a/src/invidious/routes/api/v1/videos.cr +++ b/src/invidious/routes/api/v1/videos.cr @@ -218,11 +218,11 @@ module Invidious::Routes::API::V1::Videos template_path = sb.proxied_url.path # Initialize cue timing variables - # NOTE: videojs-vtt-thumbnails gets lost when the start and end times are not 0:00:000.000 - # TODO: Use proper end time when videojs-vtt-thumbnails is fixed + # NOTE: videojs-vtt-thumbnails gets lost when the cue times don't overlap + # (i.e: if cue[n] end time is 1:06:25.000, cue[n+1] start time should be 1:06:25.000) time_delta = sb.interval.milliseconds start_time = 0.milliseconds - end_time = 0.milliseconds # time_delta - 1.milliseconds + end_time = time_delta # Build a VTT file for VideoJS-vtt plugin vtt_file = WebVTT.build do |vtt| @@ -237,9 +237,8 @@ module Invidious::Routes::API::V1::Videos vtt.cue(start_time, end_time, work_url.to_s) - # TODO: uncomment these when videojs-vtt-thumbnails is fixed - # start_time += time_delta - # end_time += time_delta + start_time += time_delta + end_time += time_delta end end end From ccecc6d318ea80b2af3bf379b33700dcb6e16c97 Mon Sep 17 00:00:00 2001 From: syeopite <70992037+syeopite@users.noreply.github.com> Date: Sat, 24 Aug 2024 18:11:11 +0000 Subject: [PATCH 131/136] Fix lint errors introduced in #4146 and #4295 (#4876) * Ameba: Fix Naming/VariableNames Introduced in #4295 * Ameba: Fix Naming/PredicateName Introduced in #4146 --- src/invidious/channels/about.cr | 6 +++--- src/invidious/routes/search.cr | 2 +- src/invidious/search/query.cr | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/invidious/channels/about.cr b/src/invidious/channels/about.cr index 1380044a..13909527 100644 --- a/src/invidious/channels/about.cr +++ b/src/invidious/channels/about.cr @@ -50,12 +50,12 @@ def get_about_info(ucid, locale) : AboutChannel total_views = 0_i64 joined = Time.unix(0) - if ageGate = initdata.dig?("contents", "twoColumnBrowseResultsRenderer", "tabs", 0, "tabRenderer", "content", "sectionListRenderer", "contents", 0, "channelAgeGateRenderer") + if age_gate_renderer = initdata.dig?("contents", "twoColumnBrowseResultsRenderer", "tabs", 0, "tabRenderer", "content", "sectionListRenderer", "contents", 0, "channelAgeGateRenderer") description_node = nil - author = ageGate["channelTitle"].as_s + author = age_gate_renderer["channelTitle"].as_s ucid = initdata.dig("responseContext", "serviceTrackingParams", 0, "params", 0, "value").as_s author_url = "https://www.youtube.com/channel/#{ucid}" - author_thumbnail = ageGate.dig("avatar", "thumbnails", 0, "url").as_s + author_thumbnail = age_gate_renderer.dig("avatar", "thumbnails", 0, "url").as_s banner = nil is_family_friendly = false is_age_gated = true diff --git a/src/invidious/routes/search.cr b/src/invidious/routes/search.cr index 85aa1c7e..44970922 100644 --- a/src/invidious/routes/search.cr +++ b/src/invidious/routes/search.cr @@ -53,7 +53,7 @@ module Invidious::Routes::Search # An URL was copy/pasted in the search box. # Redirect the user to the appropriate page. - if query.is_url? + if query.url? return env.redirect UrlSanitizer.process(query.text).to_s end diff --git a/src/invidious/search/query.cr b/src/invidious/search/query.cr index a93bb3f9..c8e8cf7f 100644 --- a/src/invidious/search/query.cr +++ b/src/invidious/search/query.cr @@ -149,7 +149,7 @@ module Invidious::Search end # Checks if the query is a standalone URL - def is_url? : Bool + def url? : Bool # If the smart features have been inhibited, don't go further. return false if @inhibit_ssf From 80958aa0d8f5d29d9e7e382143e1999e31474711 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sun, 25 Aug 2024 21:18:11 +0200 Subject: [PATCH 132/136] Release v2.20240825 --- CHANGELOG.md | 159 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 158 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6f67160..846d39a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,163 @@ # CHANGELOG -## 2024-04-26 +## v2.20240825 (2024-08-25) + +### New features & important changes + +#### For users + +* The search bar now has a button that you can click! +* Youtube URLs can be pasted directly in the search bar. Prepend search query with a + backslash (`\`) to disable that feature (useful if you need to search for a video whose + title contains some youtube URL). +* On the channel page the "streams" tab can be sorted by either: "newest", "oldest" or "popular" +* Lots of translations have been updated (thanks to our contributors on Weblate!) +* Videos embedded in local HTML files (e.g: a webpage saved from a blog) can now be played + +#### For instance owners + +* Invidious now has the ability to provide a `po_token` and `visitordata` to Youtube in order to + circumvent current Youtube restrictions. +* Invidious can use an (optional) external signature server like [inv_sig_helper]. Please note that + some videos can't be played without that signature server. +* The Helm charts were moved to a separate repo: https://github.com/iv-org/invidious-helm-chart +* We have changed how containers are released: the `latest` tag now tracks tagged releases, whereas + the `master` tag tracks the most recent commits of the `master` branch ("nightly" builds). + +[inv_sig_helper]: https://github.com/iv-org/inv_sig_helper + +#### For developpers + +* The versions of Crystal that we test in CI/CD are now: `1.9.2`, `1.10.1`, `1.11.2`, `1.12.1`. + Please note that due to a bug in the `libxml` bindings (See [#4256]), versions prior to `1.10.0` + are not recommended to use. +* Thanks to @syeopite, the code is now [ameba] compliant. +* Ameba is part of our CI/CD pipeline, and its rules will be enforced in future PRs. +* The transcript code has been rewritten to permit transcripts as a feature rather than being + only a workaround for captions. Trancripts feature is coming soon! +* Various fixes regarding the logic interacting with Youtube +* The `sort_by` parameter can be used on the `/api/v1/channels/{id}/streams` endpoint. Accepted + values are: "newest", "oldest" and "popular" + +[ameba]: https://github.com/crystal-ameba/ameba +[#4256]: https://github.com/iv-org/invidious/issues/4256 + + +### Bugs fixed + +#### User-side + +* Channels: fixed broken "subscribers" and "views" counters +* Watch page: playback position is reset at the end of a video, so that the next time this video + is watched, it will start from the beginning rather than 15 seconds before the end +* Watch page: the items in the "add to playlist" drop down are now sorted alphabetically +* Videos: the "genre" URL is now always pointing to a valid webpage +* Playlists: Fixed `Could not parse N episodes` error on podcast playlists +* All external links should now have the [`rel`] attibute set to `noreferrer noopener` for + increased privacy. +* Preferences: Fixed the admin-only "modified source code" input being ignored +* Watch/channel pages: use the full image URL in `og:image` and `twitter:image` meta tags + +[`rel`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel + +#### API + +* fixed the `local` parameter not applying to `formatStreams` on `/api/v1/videos/{id}` +* fixed an `Index out of bounds` error hapenning when a playlist had no videos +* fixed duplicated query parameters in proxied video URLs +* Return actual video height/width/fps rather than hard coded values +* Fixed the `/api/v1/popular` endpoint not returning a proper error code/message when the + popular page/endpoint are disabled. + + +### Full list of pull requests merged since the last release (newest first) + +* HTML: Sort playlists alphabetically in watch page drop down ([#4853], by @SamantazFox) +* Videos: Fix XSS vulnerability in description/comments ([#4852], thanks _anonymous_) +* YtAPI: Bump client versions ([#4849], by @SamantazFox) +* SigHelper: Fix inverted time comparison in 'check_update' ([#4845], by @SamantazFox) +* Storyboards: Various fixes and code cleaning ([#4153], by SamantazFox) +* Fix lint errors introduced in #4146 and #4295 ([#4876], thanks @syeopite) +* Search: Add support for Youtube URLs ([#4146], by @SamantazFox) +* Channel: Render age restricted channels ([#4295], thanks @ChunkyProgrammer) +* Ameba: Miscellaneous fixes ([#4807], thanks @syeopite) +* API: Proxy formatStreams URLs too ([#4859], thanks @colinleroy) +* UI: Add search button to search bar ([#4706], thanks @thansk) +* Add ability to set po_token and visitordata ID ([#4789], thanks @unixfox) +* Add support for an external signature server ([#4772], by @SamantazFox) +* Ameba: Fix Naming/VariableNames ([#4790], thanks @syeopite) +* Translations update from Hosted Weblate ([#4659]) +* Ameba: Fix Lint/UselessAssign ([#4795], thanks @syeopite) +* HTML: Add rel="noreferrer noopener" to external links ([#4667], thanks @ulmemxpoc) +* Remove unused methods in Invidious::LogHandler ([#4812], thanks @syeopite) +* Ameba: Fix Lint/NotNilAfterNoBang ([#4796], thanks @syeopite) +* Ameba: Fix unused argument Lint warnings ([#4805], thanks @syeopite) +* Ameba: i18next.cr fixes ([#4806], thanks @syeopite) +* Ameba: Disable rules ([#4792], thanks @syeopite) +* Channel: parse subscriber count and channel banner ([#4785], thanks @ChunkyProgrammer) +* Player: Fix playback position of already watched videos ([#4731], thanks @Fijxu) +* Videos: Fix genre url being unusable ([#4717], thanks @meatball133) +* API: Fix out of bound error on empty playlists ([#4696], thanks @Fijxu) +* Handle playlists cataloged as Podcast ([#4695], thanks @Fijxu) +* API: Fix duplicated query parameters in proxied video URLs ([#4587], thanks @absidue) +* API: Return actual stream height, width and fps ([#4586], thanks @absidue) +* Preferences: Fix handling of modified source code URL ([#4437], thanks @nooptek) +* API: Fix URL for vtt subtitles ([#4221], thanks @karelrooted) +* Channels: Add sort options to streams ([#4224], thanks @src-tinkerer) +* API: Fix error code for disabled popular endpoint ([#4296], thanks @iBicha) +* Allow embedding videos in local HTML files ([#4450], thanks @tomasz1986) +* CI: Bump Crystal version matrix ([#4654], by @SamantazFox) +* YtAPI: Remove API keys like official clients ([#4655], by @SamantazFox) +* HTML: Use full URL in the og:image property ([#4675], thanks @Fijxu) +* Rewrite transcript logic to be more generic ([#4747], thanks @syeopite) +* CI: Run Ameba ([#4753], thanks @syeopite) +* CI: Add release based containers ([#4763], thanks @syeopite) +* move helm chart to a dedicated github repository ([#4711], thanks @unixfox) + +[#4146]: https://github.com/iv-org/invidious/pull/4146 +[#4153]: https://github.com/iv-org/invidious/pull/4153 +[#4221]: https://github.com/iv-org/invidious/pull/4221 +[#4224]: https://github.com/iv-org/invidious/pull/4224 +[#4295]: https://github.com/iv-org/invidious/pull/4295 +[#4296]: https://github.com/iv-org/invidious/pull/4296 +[#4437]: https://github.com/iv-org/invidious/pull/4437 +[#4450]: https://github.com/iv-org/invidious/pull/4450 +[#4586]: https://github.com/iv-org/invidious/pull/4586 +[#4587]: https://github.com/iv-org/invidious/pull/4587 +[#4654]: https://github.com/iv-org/invidious/pull/4654 +[#4655]: https://github.com/iv-org/invidious/pull/4655 +[#4659]: https://github.com/iv-org/invidious/pull/4659 +[#4667]: https://github.com/iv-org/invidious/pull/4667 +[#4675]: https://github.com/iv-org/invidious/pull/4675 +[#4695]: https://github.com/iv-org/invidious/pull/4695 +[#4696]: https://github.com/iv-org/invidious/pull/4696 +[#4706]: https://github.com/iv-org/invidious/pull/4706 +[#4711]: https://github.com/iv-org/invidious/pull/4711 +[#4717]: https://github.com/iv-org/invidious/pull/4717 +[#4731]: https://github.com/iv-org/invidious/pull/4731 +[#4747]: https://github.com/iv-org/invidious/pull/4747 +[#4753]: https://github.com/iv-org/invidious/pull/4753 +[#4763]: https://github.com/iv-org/invidious/pull/4763 +[#4772]: https://github.com/iv-org/invidious/pull/4772 +[#4785]: https://github.com/iv-org/invidious/pull/4785 +[#4789]: https://github.com/iv-org/invidious/pull/4789 +[#4790]: https://github.com/iv-org/invidious/pull/4790 +[#4792]: https://github.com/iv-org/invidious/pull/4792 +[#4795]: https://github.com/iv-org/invidious/pull/4795 +[#4796]: https://github.com/iv-org/invidious/pull/4796 +[#4805]: https://github.com/iv-org/invidious/pull/4805 +[#4806]: https://github.com/iv-org/invidious/pull/4806 +[#4807]: https://github.com/iv-org/invidious/pull/4807 +[#4812]: https://github.com/iv-org/invidious/pull/4812 +[#4845]: https://github.com/iv-org/invidious/pull/4845 +[#4849]: https://github.com/iv-org/invidious/pull/4849 +[#4852]: https://github.com/iv-org/invidious/pull/4852 +[#4853]: https://github.com/iv-org/invidious/pull/4853 +[#4859]: https://github.com/iv-org/invidious/pull/4859 +[#4876]: https://github.com/iv-org/invidious/pull/4876 + + +## v2.20240427 (2024-04-27) Major bug fixes: * Videos: Use android test suite client (#4650, thanks @SamantazFox) From cec905e95e036323b60911252a061c50f1664c03 Mon Sep 17 00:00:00 2001 From: syeopite <70992037+syeopite@users.noreply.github.com> Date: Sun, 25 Aug 2024 19:55:52 +0000 Subject: [PATCH 133/136] Allow manual trigger of release-container build (#4877) --- .github/workflows/build-stable-container.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-stable-container.yml b/.github/workflows/build-stable-container.yml index b5fbc705..4f7afab3 100644 --- a/.github/workflows/build-stable-container.yml +++ b/.github/workflows/build-stable-container.yml @@ -1,6 +1,7 @@ name: Build and release container on: + workflow_dispatch: push: tags: - "v*" From 3e17d04875570448edf42641175d297ec2ba2aa1 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sun, 25 Aug 2024 22:30:46 +0200 Subject: [PATCH 134/136] Release v2.20240825.1 --- CHANGELOG.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 846d39a1..769ddd69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,18 @@ # CHANGELOG -## v2.20240825 (2024-08-25) +## v2.20240825.1 (2024-08-25) + +Add patch component to be [semver] compliant and make github actions happy. + +[semver]: https://semver.org/ + +### Full list of pull requests merged since the last release (newest first) + +Allow manual trigger of release-container build (#4877, thanks @syeopite) + + + +## v2.20240825.0 (2024-08-25) ### New features & important changes From 4f066e880c8ea7fb34fa4cb64c3e81a04f272de2 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Mon, 26 Aug 2024 21:55:43 +0200 Subject: [PATCH 135/136] CI: Fix docker container tags --- .github/workflows/build-stable-container.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-stable-container.yml b/.github/workflows/build-stable-container.yml index 4f7afab3..d2d106b6 100644 --- a/.github/workflows/build-stable-container.yml +++ b/.github/workflows/build-stable-container.yml @@ -47,9 +47,11 @@ jobs: uses: docker/metadata-action@v5 with: images: quay.io/invidious/invidious + flavor: | + latest=false tags: | type=semver,pattern={{version}} - type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} + type=raw,value=latest labels: | quay.expires-after=12w @@ -71,10 +73,11 @@ jobs: with: images: quay.io/invidious/invidious flavor: | + latest=false suffix=-arm64 tags: | type=semver,pattern={{version}} - type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} + type=raw,value=latest labels: | quay.expires-after=12w From 4782a6703819e0babfa4792892b691dd096eeac3 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Mon, 26 Aug 2024 22:52:50 +0200 Subject: [PATCH 136/136] Release v2.20240825.2 --- CHANGELOG.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 769ddd69..2cc5b05c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # CHANGELOG + +## v2.20240825.2 (2024-08-26) + +This releases fixes the container tags pushed on quay.io. +Previously, the ARM64 build was released under the `latest` tag, instead of `latest-arm64`. + +### Full list of pull requests merged since the last release (newest first) + +CI: Fix docker container tags ([#4883], by @SamantazFox) + +[#4877]: https://github.com/iv-org/invidious/pull/4877 + + ## v2.20240825.1 (2024-08-25) Add patch component to be [semver] compliant and make github actions happy. @@ -8,8 +21,9 @@ Add patch component to be [semver] compliant and make github actions happy. ### Full list of pull requests merged since the last release (newest first) -Allow manual trigger of release-container build (#4877, thanks @syeopite) +Allow manual trigger of release-container build ([#4877], thanks @syeopite) +[#4877]: https://github.com/iv-org/invidious/pull/4877 ## v2.20240825.0 (2024-08-25)