From e613e3c08c0b807e012ce08e41afa565d57ed39d Mon Sep 17 00:00:00 2001 From: Richard Lora Date: Tue, 30 Apr 2024 16:34:40 -0400 Subject: [PATCH 01/33] feat: optional `trending_enabled` and `search_enabled` params added --- config/config.example.yml | 16 ++++++ src/invidious/config.cr | 2 + src/invidious/routes/api/v1/feeds.cr | 5 ++ src/invidious/routes/api/v1/search.cr | 5 ++ src/invidious/routes/feeds.cr | 25 +++++---- src/invidious/routes/preferences.cr | 8 +++ src/invidious/routes/search.cr | 67 +++++++++++++----------- src/invidious/views/user/preferences.ecr | 9 ++++ 8 files changed, 96 insertions(+), 41 deletions(-) diff --git a/config/config.example.yml b/config/config.example.yml index 38085a20..9f0c252a 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -210,6 +210,22 @@ https_only: false ## #popular_enabled: true +## +## Enable/Disable the "Trending" tab on the main page. +## +## Accepted values: true, false +## Default: true +## +#trending_enabled: true + +## +## Enable/Disable "Search" on the main page. +## +## Accepted values: true, false +## Default: true +## +#search_enabled: true + ## ## Enable/Disable statstics (available at /api/v1/stats). ## The following data is available: diff --git a/src/invidious/config.cr b/src/invidious/config.cr index 09c2168b..f6146cd7 100644 --- a/src/invidious/config.cr +++ b/src/invidious/config.cr @@ -91,6 +91,8 @@ class Config # Subscribe to channels using PubSubHubbub (requires domain, hmac_key) property use_pubsub_feeds : Bool | Int32 = false property popular_enabled : Bool = true + property trending_enabled : Bool = true + property search_enabled : Bool = true property captcha_enabled : Bool = true property login_enabled : Bool = true property registration_enabled : Bool = true diff --git a/src/invidious/routes/api/v1/feeds.cr b/src/invidious/routes/api/v1/feeds.cr index 41865f34..f6feab5f 100644 --- a/src/invidious/routes/api/v1/feeds.cr +++ b/src/invidious/routes/api/v1/feeds.cr @@ -4,6 +4,11 @@ module Invidious::Routes::API::V1::Feeds env.response.content_type = "application/json" + if !CONFIG.trending_enabled + error_message = {"error" => "Administrator has disabled this endpoint."}.to_json + haltf env, 400, error_message + end + region = env.params.query["region"]? trending_type = env.params.query["type"]? diff --git a/src/invidious/routes/api/v1/search.cr b/src/invidious/routes/api/v1/search.cr index 2922b060..526914b0 100644 --- a/src/invidious/routes/api/v1/search.cr +++ b/src/invidious/routes/api/v1/search.cr @@ -5,6 +5,11 @@ module Invidious::Routes::API::V1::Search env.response.content_type = "application/json" + if !CONFIG.search_enabled + error_message = {"error" => "Administrator has disabled this endpoint."}.to_json + haltf env, 400, error_message + end + query = Invidious::Search::Query.new(env.params.query, :regular, region) begin diff --git a/src/invidious/routes/feeds.cr b/src/invidious/routes/feeds.cr index e20a7139..22a23a7e 100644 --- a/src/invidious/routes/feeds.cr +++ b/src/invidious/routes/feeds.cr @@ -44,20 +44,25 @@ module Invidious::Routes::Feeds def self.trending(env) locale = env.get("preferences").as(Preferences).locale + + if CONFIG.trending_enabled + trending_type = env.params.query["type"]? + trending_type ||= "Default" - trending_type = env.params.query["type"]? - trending_type ||= "Default" + region = env.params.query["region"]? + region ||= env.get("preferences").as(Preferences).region - region = env.params.query["region"]? - region ||= env.get("preferences").as(Preferences).region + begin + trending, plid = fetch_trending(trending_type, region, locale) + rescue ex + return error_template(500, ex) + end - begin - trending, plid = fetch_trending(trending_type, region, locale) - rescue ex - return error_template(500, ex) + templated "feeds/trending" + else + message = translate(locale, "The Trending feed has been disabled by the administrator.") + templated "message" end - - templated "feeds/trending" end def self.subscriptions(env) diff --git a/src/invidious/routes/preferences.cr b/src/invidious/routes/preferences.cr index 112535bd..9aee0d8c 100644 --- a/src/invidious/routes/preferences.cr +++ b/src/invidious/routes/preferences.cr @@ -198,6 +198,14 @@ module Invidious::Routes::PreferencesRoute popular_enabled ||= "off" CONFIG.popular_enabled = popular_enabled == "on" + trending_enabled = env.params.body["trending_enabled"]?.try &.as(String) + trending_enabled ||= "off" + CONFIG.trending_enabled = trending_enabled == "on" + + search_enabled = env.params.body["search_enabled"]?.try &.as(String) + search_enabled ||= "off" + CONFIG.search_enabled = search_enabled == "on" + captcha_enabled = env.params.body["captcha_enabled"]?.try &.as(String) captcha_enabled ||= "off" CONFIG.captcha_enabled = captcha_enabled == "on" diff --git a/src/invidious/routes/search.cr b/src/invidious/routes/search.cr index 5be33533..ae7d9833 100644 --- a/src/invidious/routes/search.cr +++ b/src/invidious/routes/search.cr @@ -40,41 +40,46 @@ module Invidious::Routes::Search prefs = env.get("preferences").as(Preferences) locale = prefs.locale - region = env.params.query["region"]? || prefs.region + if CONFIG.search_enabled + region = env.params.query["region"]? || prefs.region - query = Invidious::Search::Query.new(env.params.query, :regular, region) + query = Invidious::Search::Query.new(env.params.query, :regular, region) - if query.empty? - # Display the full page search box implemented in #1977 - env.set "search", "" - templated "search_homepage", navbar_search: false - else - user = env.get? "user" - - begin - items = query.process - rescue ex : ChannelSearchException - return error_template(404, "Unable to find channel with id of '#{HTML.escape(ex.channel)}'. Are you sure that's an actual channel id? It should look like 'UC4QobU6STFB0P71PMvOGN5A'.") - rescue ex - return error_template(500, ex) - end - - redirect_url = Invidious::Frontend::Misc.redirect_url(env) - - # Pagination - page_nav_html = Frontend::Pagination.nav_numeric(locale, - base_url: "/search?#{query.to_http_params}", - current_page: query.page, - show_next: (items.size >= 20) - ) - - if query.type == Invidious::Search::Query::Type::Channel - env.set "search", "channel:#{query.channel} #{query.text}" + if query.empty? + # Display the full page search box implemented in #1977 + env.set "search", "" + templated "search_homepage", navbar_search: false else - env.set "search", query.text - end + user = env.get? "user" - templated "search" + begin + items = query.process + rescue ex : ChannelSearchException + return error_template(404, "Unable to find channel with id of '#{HTML.escape(ex.channel)}'. Are you sure that's an actual channel id? It should look like 'UC4QobU6STFB0P71PMvOGN5A'.") + rescue ex + return error_template(500, ex) + end + + redirect_url = Invidious::Frontend::Misc.redirect_url(env) + + # Pagination + page_nav_html = Frontend::Pagination.nav_numeric(locale, + base_url: "/search?#{query.to_http_params}", + current_page: query.page, + show_next: (items.size >= 20) + ) + + if query.type == Invidious::Search::Query::Type::Channel + env.set "search", "channel:#{query.channel} #{query.text}" + else + env.set "search", query.text + end + + templated "search" + end + else + message = translate(locale, "Search has been disabled by the administrator.") + templated "message" end end diff --git a/src/invidious/views/user/preferences.ecr b/src/invidious/views/user/preferences.ecr index 55349c5a..4cda8421 100644 --- a/src/invidious/views/user/preferences.ecr +++ b/src/invidious/views/user/preferences.ecr @@ -287,6 +287,15 @@ checked<% end %>> +
+ + checked<% end %>> +
+ +
+ + checked<% end %>> +
From 5fa293541b05aa9fc0be404e5e872cb991cbe423 Mon Sep 17 00:00:00 2001 From: Richard Lora Date: Fri, 3 May 2024 10:36:43 -0400 Subject: [PATCH 02/33] fix(feeds.cr): http status code --- src/invidious/routes/api/v1/feeds.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/invidious/routes/api/v1/feeds.cr b/src/invidious/routes/api/v1/feeds.cr index f6feab5f..3a483526 100644 --- a/src/invidious/routes/api/v1/feeds.cr +++ b/src/invidious/routes/api/v1/feeds.cr @@ -6,7 +6,7 @@ module Invidious::Routes::API::V1::Feeds if !CONFIG.trending_enabled error_message = {"error" => "Administrator has disabled this endpoint."}.to_json - haltf env, 400, error_message + haltf env, 403, error_message end region = env.params.query["region"]? @@ -36,7 +36,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 3abf21625cc88d92815814a6217d7a173eec1336 Mon Sep 17 00:00:00 2001 From: Richard Lora Date: Fri, 3 May 2024 10:38:57 -0400 Subject: [PATCH 03/33] fix(search.cr): http status code --- src/invidious/routes/api/v1/search.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/routes/api/v1/search.cr b/src/invidious/routes/api/v1/search.cr index 526914b0..3c44a989 100644 --- a/src/invidious/routes/api/v1/search.cr +++ b/src/invidious/routes/api/v1/search.cr @@ -7,7 +7,7 @@ module Invidious::Routes::API::V1::Search if !CONFIG.search_enabled error_message = {"error" => "Administrator has disabled this endpoint."}.to_json - haltf env, 400, error_message + haltf env, 403, error_message end query = Invidious::Search::Query.new(env.params.query, :regular, region) From e0f20b06418e1a93ae6c0e27e8e2871970c92803 Mon Sep 17 00:00:00 2001 From: Richard Lora Date: Fri, 3 May 2024 14:00:28 -0400 Subject: [PATCH 04/33] style(feeds.cr): fix code formatting --- src/invidious/routes/feeds.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/routes/feeds.cr b/src/invidious/routes/feeds.cr index 22a23a7e..693becb8 100644 --- a/src/invidious/routes/feeds.cr +++ b/src/invidious/routes/feeds.cr @@ -44,7 +44,7 @@ module Invidious::Routes::Feeds def self.trending(env) locale = env.get("preferences").as(Preferences).locale - + if CONFIG.trending_enabled trending_type = env.params.query["type"]? trending_type ||= "Default" From da422fae05da70740be881c12f82c955f9fb14b7 Mon Sep 17 00:00:00 2001 From: Richard Lora Date: Sun, 30 Jun 2024 18:05:26 -0400 Subject: [PATCH 05/33] feat(invidious): specific pages are disabled with the use of an array --- config/config.example.yml | 27 ++++++------------------ src/invidious.cr | 2 +- src/invidious/config.cr | 9 +++++--- src/invidious/routes/api/v1/feeds.cr | 4 ++-- src/invidious/routes/api/v1/search.cr | 2 +- src/invidious/routes/feeds.cr | 4 ++-- src/invidious/routes/preferences.cr | 19 ++++++----------- src/invidious/routes/search.cr | 4 ++-- src/invidious/views/user/preferences.ecr | 6 +++--- 9 files changed, 30 insertions(+), 47 deletions(-) diff --git a/config/config.example.yml b/config/config.example.yml index 9f0c252a..8749ec57 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -203,28 +203,13 @@ https_only: false # ----------------------------- ## -## Enable/Disable the "Popular" tab on the main page. +## Enable/Disable specific pages on the main page. +## Example: +## pages_enabled: +## trending: true +## popular: true +## search: true ## -## Accepted values: true, false -## Default: true -## -#popular_enabled: true - -## -## Enable/Disable the "Trending" tab on the main page. -## -## Accepted values: true, false -## Default: true -## -#trending_enabled: true - -## -## Enable/Disable "Search" on the main page. -## -## Accepted values: true, false -## Default: true -## -#search_enabled: true ## ## Enable/Disable statstics (available at /api/v1/stats). diff --git a/src/invidious.cr b/src/invidious.cr index e0bd0101..d0a02a3d 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -176,7 +176,7 @@ if (CONFIG.use_pubsub_feeds.is_a?(Bool) && CONFIG.use_pubsub_feeds.as(Bool)) || Invidious::Jobs.register Invidious::Jobs::SubscribeToFeedsJob.new(PG_DB, HMAC_KEY) end -if CONFIG.popular_enabled +if CONFIG.page_enabled?("popular") Invidious::Jobs.register Invidious::Jobs::PullPopularVideosJob.new(PG_DB) end diff --git a/src/invidious/config.cr b/src/invidious/config.cr index f6146cd7..085aeef8 100644 --- a/src/invidious/config.cr +++ b/src/invidious/config.cr @@ -90,9 +90,7 @@ class Config property domain : String? # Subscribe to channels using PubSubHubbub (requires domain, hmac_key) property use_pubsub_feeds : Bool | Int32 = false - property popular_enabled : Bool = true - property trending_enabled : Bool = true - property search_enabled : Bool = true + property pages_enabled : Hash(String, Bool) = {"trending" => true, "popular" => true, "search" => true} property captcha_enabled : Bool = true property login_enabled : Bool = true property registration_enabled : Bool = true @@ -154,6 +152,11 @@ class Config end end + def page_enabled?(page : String) : Bool + @pages_enabled[page]? || false + end + + def self.load # Load config from file or YAML string env var env_config_file = "INVIDIOUS_CONFIG_FILE" diff --git a/src/invidious/routes/api/v1/feeds.cr b/src/invidious/routes/api/v1/feeds.cr index 3a483526..bfec4fb6 100644 --- a/src/invidious/routes/api/v1/feeds.cr +++ b/src/invidious/routes/api/v1/feeds.cr @@ -4,7 +4,7 @@ module Invidious::Routes::API::V1::Feeds env.response.content_type = "application/json" - if !CONFIG.trending_enabled + if !CONFIG.page_enabled?("trending") error_message = {"error" => "Administrator has disabled this endpoint."}.to_json haltf env, 403, error_message end @@ -34,7 +34,7 @@ module Invidious::Routes::API::V1::Feeds env.response.content_type = "application/json" - if !CONFIG.popular_enabled + if !CONFIG.page_enabled?("popular") error_message = {"error" => "Administrator has disabled this endpoint."}.to_json haltf env, 403, error_message end diff --git a/src/invidious/routes/api/v1/search.cr b/src/invidious/routes/api/v1/search.cr index 3c44a989..48df3a2b 100644 --- a/src/invidious/routes/api/v1/search.cr +++ b/src/invidious/routes/api/v1/search.cr @@ -5,7 +5,7 @@ module Invidious::Routes::API::V1::Search env.response.content_type = "application/json" - if !CONFIG.search_enabled + if !CONFIG.page_enabled?("search") error_message = {"error" => "Administrator has disabled this endpoint."}.to_json haltf env, 403, error_message end diff --git a/src/invidious/routes/feeds.cr b/src/invidious/routes/feeds.cr index 693becb8..114b9edc 100644 --- a/src/invidious/routes/feeds.cr +++ b/src/invidious/routes/feeds.cr @@ -34,7 +34,7 @@ module Invidious::Routes::Feeds def self.popular(env) locale = env.get("preferences").as(Preferences).locale - if CONFIG.popular_enabled + if CONFIG.page_enabled?("popular") templated "feeds/popular" else message = translate(locale, "The Popular feed has been disabled by the administrator.") @@ -45,7 +45,7 @@ module Invidious::Routes::Feeds def self.trending(env) locale = env.get("preferences").as(Preferences).locale - if CONFIG.trending_enabled + if CONFIG.page_enabled?("trending") trending_type = env.params.query["type"]? trending_type ||= "Default" diff --git a/src/invidious/routes/preferences.cr b/src/invidious/routes/preferences.cr index 9aee0d8c..9106d4cf 100644 --- a/src/invidious/routes/preferences.cr +++ b/src/invidious/routes/preferences.cr @@ -194,17 +194,12 @@ module Invidious::Routes::PreferencesRoute end CONFIG.default_user_preferences.feed_menu = admin_feed_menu - popular_enabled = env.params.body["popular_enabled"]?.try &.as(String) - popular_enabled ||= "off" - CONFIG.popular_enabled = popular_enabled == "on" - - trending_enabled = env.params.body["trending_enabled"]?.try &.as(String) - trending_enabled ||= "off" - CONFIG.trending_enabled = trending_enabled == "on" - - search_enabled = env.params.body["search_enabled"]?.try &.as(String) - search_enabled ||= "off" - CONFIG.search_enabled = search_enabled == "on" + pages_enabled = { + "popular" => (env.params.body["popular_enabled"]?.try &.as(String) || "off") == "on", + "trending" => (env.params.body["trending_enabled"]?.try &.as(String) || "off") == "on", + "search" => (env.params.body["search_enabled"]?.try &.as(String) || "off") == "on" + } + CONFIG.pages_enabled = pages_enabled captcha_enabled = env.params.body["captcha_enabled"]?.try &.as(String) captcha_enabled ||= "off" @@ -355,4 +350,4 @@ module Invidious::Routes::PreferencesRoute env.redirect referer end -end +end \ No newline at end of file diff --git a/src/invidious/routes/search.cr b/src/invidious/routes/search.cr index ae7d9833..f825ffc7 100644 --- a/src/invidious/routes/search.cr +++ b/src/invidious/routes/search.cr @@ -40,7 +40,7 @@ module Invidious::Routes::Search prefs = env.get("preferences").as(Preferences) locale = prefs.locale - if CONFIG.search_enabled + if CONFIG.page_enabled?("search") region = env.params.query["region"]? || prefs.region query = Invidious::Search::Query.new(env.params.query, :regular, region) @@ -115,4 +115,4 @@ module Invidious::Routes::Search templated "hashtag" end -end +end \ No newline at end of file diff --git a/src/invidious/views/user/preferences.ecr b/src/invidious/views/user/preferences.ecr index 4cda8421..564eca9e 100644 --- a/src/invidious/views/user/preferences.ecr +++ b/src/invidious/views/user/preferences.ecr @@ -284,17 +284,17 @@
- checked<% end %>> + checked<% end %>>
- checked<% end %>> + checked<% end %>>
- checked<% end %>> + checked<% end %>>
From ba7e504fd1eb6fe2932089a3de267a24cdc037fa Mon Sep 17 00:00:00 2001 From: Richard Lora Date: Sun, 30 Jun 2024 18:11:56 -0400 Subject: [PATCH 06/33] chore(search.cr): add newline at end of file --- src/invidious/routes/search.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/routes/search.cr b/src/invidious/routes/search.cr index f825ffc7..8c500e02 100644 --- a/src/invidious/routes/search.cr +++ b/src/invidious/routes/search.cr @@ -115,4 +115,4 @@ module Invidious::Routes::Search templated "hashtag" end -end \ No newline at end of file +end From bfe51590b1bae0a8a86ad68c0b35c3ba63e4d390 Mon Sep 17 00:00:00 2001 From: Richard Lora Date: Mon, 1 Jul 2024 14:48:55 -0400 Subject: [PATCH 07/33] chore(lint): format with crystal tool --- src/invidious/config.cr | 1 - src/invidious/routes/preferences.cr | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/invidious/config.cr b/src/invidious/config.cr index 085aeef8..4adadc48 100644 --- a/src/invidious/config.cr +++ b/src/invidious/config.cr @@ -156,7 +156,6 @@ class Config @pages_enabled[page]? || false end - def self.load # Load config from file or YAML string env var env_config_file = "INVIDIOUS_CONFIG_FILE" diff --git a/src/invidious/routes/preferences.cr b/src/invidious/routes/preferences.cr index 9106d4cf..1dfc472a 100644 --- a/src/invidious/routes/preferences.cr +++ b/src/invidious/routes/preferences.cr @@ -195,9 +195,9 @@ module Invidious::Routes::PreferencesRoute CONFIG.default_user_preferences.feed_menu = admin_feed_menu pages_enabled = { - "popular" => (env.params.body["popular_enabled"]?.try &.as(String) || "off") == "on", + "popular" => (env.params.body["popular_enabled"]?.try &.as(String) || "off") == "on", "trending" => (env.params.body["trending_enabled"]?.try &.as(String) || "off") == "on", - "search" => (env.params.body["search_enabled"]?.try &.as(String) || "off") == "on" + "search" => (env.params.body["search_enabled"]?.try &.as(String) || "off") == "on", } CONFIG.pages_enabled = pages_enabled @@ -350,4 +350,4 @@ module Invidious::Routes::PreferencesRoute env.redirect referer end -end \ No newline at end of file +end From 2ec34f36f5e3bd1f5905b7d970281ee52d30db91 Mon Sep 17 00:00:00 2001 From: Richard Lora Date: Mon, 1 Jul 2024 19:32:53 -0400 Subject: [PATCH 08/33] Update config/config.example.yml Co-authored-by: Samantaz Fox --- config/config.example.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/config/config.example.yml b/config/config.example.yml index 8749ec57..2b0e2323 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -204,11 +204,11 @@ https_only: false ## ## Enable/Disable specific pages on the main page. -## Example: -## pages_enabled: -## trending: true -## popular: true -## search: true +## +#pages_enabled: +# trending: true +# popular: true +# search: true ## ## From b0cbd29563d3d03d8e978dab24270f93a5724a5a Mon Sep 17 00:00:00 2001 From: Richard Lora Date: Mon, 1 Jul 2024 19:36:14 -0400 Subject: [PATCH 09/33] feat(en-US.json): add translation key and value --- locales/en-US.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/locales/en-US.json b/locales/en-US.json index 3987f796..faf98fff 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -496,5 +496,7 @@ "toggle_theme": "Toggle Theme", "carousel_slide": "Slide {{current}} of {{total}}", "carousel_skip": "Skip the Carousel", - "carousel_go_to": "Go to slide `x`" + "carousel_go_to": "Go to slide `x`", + "preferences_trending_enabled_label": "Trending enabled: ", + "preferences_search_enabled_label": "Search enabled: " } From fdec9046b1352eb7877a24df3235a84fe7fdc3f2 Mon Sep 17 00:00:00 2001 From: Richard Lora Date: Mon, 1 Jul 2024 19:36:33 -0400 Subject: [PATCH 10/33] Update src/invidious/views/user/preferences.ecr Co-authored-by: Samantaz Fox --- src/invidious/views/user/preferences.ecr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/invidious/views/user/preferences.ecr b/src/invidious/views/user/preferences.ecr index 564eca9e..e1633cbc 100644 --- a/src/invidious/views/user/preferences.ecr +++ b/src/invidious/views/user/preferences.ecr @@ -288,12 +288,12 @@
- + checked<% end %>>
- + checked<% end %>>
From aeb2b1046d4f17c68a24cf019c92ba206e029c41 Mon Sep 17 00:00:00 2001 From: Norkz Date: Wed, 21 May 2025 19:44:35 -0400 Subject: [PATCH 11/33] feat: test --- locales/en-US.json | 6 +- src/invidious/config.cr | 35 +++++++--- src/invidious/routes/search.cr | 114 +++++++++++++-------------------- 3 files changed, 74 insertions(+), 81 deletions(-) diff --git a/locales/en-US.json b/locales/en-US.json index 2d0adc27..1aa2ab92 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -504,5 +504,7 @@ "preferences_search_enabled_label": "Search enabled: ", "timeline_parse_error_placeholder_heading": "Unable to parse item", "timeline_parse_error_placeholder_message": "Invidious encountered an error while trying to parse this item. For more information see below:", - "timeline_parse_error_show_technical_details": "Show technical details" -} + "timeline_parse_error_show_technical_details": "Show technical details", + "Search has been disabled by the administrator.": "Search has been disabled by the administrator.", + "This helps protect our community. Learn more": "This helps protect our community. Learn more" +} \ No newline at end of file diff --git a/src/invidious/config.cr b/src/invidious/config.cr index 9bf5be44..36cb40a3 100644 --- a/src/invidious/config.cr +++ b/src/invidious/config.cr @@ -111,13 +111,29 @@ class Config # Used to tell Invidious it is behind a proxy, so links to resources should be https:// property https_only : Bool? + # HMAC signing key for CSRF tokens and verifying pubsub subscriptions property hmac_key : String = "" # Domain to be used for links to resources on the site where an absolute URL is required property domain : String? # Subscribe to channels using PubSubHubbub (requires domain, hmac_key) property use_pubsub_feeds : Bool | Int32 = false - property pages_enabled : Hash(String, Bool) = {"trending" => true, "popular" => true, "search" => true} + + # ————————————————————————————————————————————————————————————————————————————————————— + # DEPRECATED: use `pages_enabled["popular"]` instead. + @[Deprecated("`popular_enabled` will be removed in a future release; use pages_enabled[\"popular\"] instead")] + property popular_enabled : Bool = true + + # Global per-page feature toggles. + # Valid keys: "trending", "popular", "search" + # If someone sets both `popular_enabled` and `pages_enabled["popular"]`, the latter takes precedence. + property pages_enabled : Hash(String, Bool) = { + "trending" => true, + "popular" => true, + "search" => true, + } + # ————————————————————————————————————————————————————————————————————————————————————— + property captcha_enabled : Bool = true property login_enabled : Bool = true property registration_enabled : Bool = true @@ -188,18 +204,21 @@ class Config when Bool return disabled when Array - if disabled.includes? option - return true - else - return false - end + disabled.includes?(option) else - return false + false end end + # Centralized page toggle with legacy fallback for `popular_enabled` def page_enabled?(page : String) : Bool - @pages_enabled[page]? || false + if pages_enabled.has_key?(page) + pages_enabled[page] + elsif page == "popular" + popular_enabled + else + true + end end def self.load diff --git a/src/invidious/routes/search.cr b/src/invidious/routes/search.cr index cbae9183..bd76775d 100644 --- a/src/invidious/routes/search.cr +++ b/src/invidious/routes/search.cr @@ -40,79 +40,51 @@ module Invidious::Routes::Search prefs = env.get("preferences").as(Preferences) locale = prefs.locale - if CONFIG.page_enabled?("search") - region = env.params.query["region"]? || prefs.region - - query = Invidious::Search::Query.new(env.params.query, :regular, region) - - if query.empty? - # Display the full page search box implemented in #1977 - env.set "search", "" - templated "search_homepage", navbar_search: false - else - user = env.get? "user" - - # An URL was copy/pasted in the search box. - # Redirect the user to the appropriate page. - if query.url? - return env.redirect UrlSanitizer.process(query.text).to_s - end - - begin - if user - items = query.process(user.as(User)) - else - items = query.process - end - rescue ex : ChannelSearchException - return error_template(404, "Unable to find channel with id of '#{HTML.escape(ex.channel)}'. Are you sure that's an actual channel id? It should look like 'UC4QobU6STFB0P71PMvOGN5A'.") - rescue ex - return error_template(500, ex) - end - - redirect_url = Invidious::Frontend::Misc.redirect_url(env) - - # Pagination - page_nav_html = Frontend::Pagination.nav_numeric(locale, - base_url: "/search?#{query.to_http_params}", - current_page: query.page, - show_next: (items.size >= 20) - ) - - if query.type == Invidious::Search::Query::Type::Channel - env.set "search", "channel:#{query.channel} #{query.text}" - else - user = env.get? "user" - - begin - items = query.process - rescue ex : ChannelSearchException - return error_template(404, "Unable to find channel with id of '#{HTML.escape(ex.channel)}'. Are you sure that's an actual channel id? It should look like 'UC4QobU6STFB0P71PMvOGN5A'.") - rescue ex - return error_template(500, ex) - end - - redirect_url = Invidious::Frontend::Misc.redirect_url(env) - - # Pagination - page_nav_html = Frontend::Pagination.nav_numeric(locale, - base_url: "/search?#{query.to_http_params}", - current_page: query.page, - show_next: (items.size >= 20) - ) - - if query.type == Invidious::Search::Query::Type::Channel - env.set "search", "channel:#{query.channel} #{query.text}" - else - env.set "search", query.text - end - - templated "search" - end - else + # if search is disabled, show the “disabled” message immediately + unless CONFIG.page_enabled?("search") message = translate(locale, "Search has been disabled by the administrator.") - templated "message" + return templated "message" end + + # otherwise, do a normal search + region = env.params.query["region"]? || prefs.region + query = Invidious::Search::Query.new(env.params.query, :regular, region) + + # empty query → show homepage + if query.empty? + env.set "search", "" + return templated "search_homepage", navbar_search: false + end + + # non‐empty query → process it + user = env.get?("user") + if query.url? + return env.redirect UrlSanitizer.process(query.text).to_s + end + + begin + items = user ? query.process(user.as(User)) : query.process + rescue ex : ChannelSearchException + return error_template 404, "Unable to find channel with id “#{HTML.escape(ex.channel)}”…" + rescue ex + return error_template 500, ex + end + + # Pagination + page_nav_html = Frontend::Pagination.nav_numeric(locale, + base_url: "/search?#{query.to_http_params}", + current_page: query.page, + show_next: (items.size >= 20) + ) + + # If it's a channel search, prefix the box; otherwise just show the text + if query.type == Invidious::Search::Query::Type::Channel + env.set "search", "channel:#{query.channel} #{query.text}" + else + env.set "search", query.text + end + + templated "search" end def self.hashtag(env : HTTP::Server::Context) From ce0a9faaaef07ec1c286f8fb7f40712c0c1727b5 Mon Sep 17 00:00:00 2001 From: Norkz Date: Wed, 21 May 2025 21:21:45 -0400 Subject: [PATCH 12/33] feat: test2 --- locales/en-US.json | 1 + src/invidious/routes/search.cr | 2 ++ 2 files changed, 3 insertions(+) diff --git a/locales/en-US.json b/locales/en-US.json index 1aa2ab92..eb0dfe2d 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -4,6 +4,7 @@ "Answer": "Answer", "Search for videos": "Search for videos", "The Popular feed has been disabled by the administrator.": "The Popular feed has been disabled by the administrator.", + "The Trending feed has been disabled by the administrator": "The Trending feed has been disabled by the administrator", "generic_channels_count": "{{count}} channel", "generic_channels_count_plural": "{{count}} channels", "generic_views_count": "{{count}} view", diff --git a/src/invidious/routes/search.cr b/src/invidious/routes/search.cr index bd76775d..eb253d2c 100644 --- a/src/invidious/routes/search.cr +++ b/src/invidious/routes/search.cr @@ -70,6 +70,8 @@ module Invidious::Routes::Search return error_template 500, ex end + redirect_url = Invidious::Frontend::Misc.redirect_url(env) + # Pagination page_nav_html = Frontend::Pagination.nav_numeric(locale, base_url: "/search?#{query.to_http_params}", From 171bac97d1742b99bdf24dfe2d37d8561e92325c Mon Sep 17 00:00:00 2001 From: Richard Lora Date: Sat, 24 May 2025 10:01:50 -0400 Subject: [PATCH 13/33] Update locales/en-US.json Co-authored-by: Fijxu --- locales/en-US.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/en-US.json b/locales/en-US.json index eb0dfe2d..de9df452 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -3,8 +3,8 @@ "Add to playlist: ": "Add to playlist: ", "Answer": "Answer", "Search for videos": "Search for videos", - "The Popular feed has been disabled by the administrator.": "The Popular feed has been disabled by the administrator.", - "The Trending feed has been disabled by the administrator": "The Trending feed has been disabled by the administrator", + "popular_page_disabled": "The Popular feed has been disabled by the administrator.", + "trending_page_disabled": "The Trending feed has been disabled by the administrator.", "generic_channels_count": "{{count}} channel", "generic_channels_count_plural": "{{count}} channels", "generic_views_count": "{{count}} view", From e1d134f6a06f9fa2f28c22766f625890da6efe59 Mon Sep 17 00:00:00 2001 From: Richard Lora Date: Sat, 24 May 2025 10:02:00 -0400 Subject: [PATCH 14/33] Update locales/en-US.json Co-authored-by: Fijxu --- locales/en-US.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/en-US.json b/locales/en-US.json index de9df452..e5d340cf 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -506,6 +506,6 @@ "timeline_parse_error_placeholder_heading": "Unable to parse item", "timeline_parse_error_placeholder_message": "Invidious encountered an error while trying to parse this item. For more information see below:", "timeline_parse_error_show_technical_details": "Show technical details", - "Search has been disabled by the administrator.": "Search has been disabled by the administrator.", + "search_page_disabled": "Search has been disabled by the administrator.", "This helps protect our community. Learn more": "This helps protect our community. Learn more" } \ No newline at end of file From 9fbce527e7a3a1bb5256c0e900c516f78bf01443 Mon Sep 17 00:00:00 2001 From: NorkzYT Date: Sat, 24 May 2025 14:20:34 +0000 Subject: [PATCH 15/33] =?UTF-8?q?=F0=9F=93=9D=20(locales/en-US.json):=20re?= =?UTF-8?q?move=20redundant=20translation=20strings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/en-US.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/locales/en-US.json b/locales/en-US.json index e5d340cf..d5276edf 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -506,6 +506,5 @@ "timeline_parse_error_placeholder_heading": "Unable to parse item", "timeline_parse_error_placeholder_message": "Invidious encountered an error while trying to parse this item. For more information see below:", "timeline_parse_error_show_technical_details": "Show technical details", - "search_page_disabled": "Search has been disabled by the administrator.", - "This helps protect our community. Learn more": "This helps protect our community. Learn more" + "search_page_disabled": "Search has been disabled by the administrator." } \ No newline at end of file From 3eeec8632d73c122ea7c1736d7bb4064318c8928 Mon Sep 17 00:00:00 2001 From: NorkzYT Date: Sat, 24 May 2025 14:50:58 +0000 Subject: [PATCH 16/33] =?UTF-8?q?=F0=9F=8C=90=20(feeds.cr,=20search.cr):?= =?UTF-8?q?=20update=20translation=20keys=20for=20disabled=20feeds=20and?= =?UTF-8?q?=20search=20messages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/invidious/routes/feeds.cr | 4 ++-- src/invidious/routes/search.cr | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/invidious/routes/feeds.cr b/src/invidious/routes/feeds.cr index 672f6280..c1852f5e 100644 --- a/src/invidious/routes/feeds.cr +++ b/src/invidious/routes/feeds.cr @@ -37,7 +37,7 @@ module Invidious::Routes::Feeds if CONFIG.page_enabled?("popular") templated "feeds/popular" else - message = translate(locale, "The Popular feed has been disabled by the administrator.") + message = translate(locale, "popular_page_disabled") templated "message" end end @@ -60,7 +60,7 @@ module Invidious::Routes::Feeds templated "feeds/trending" else - message = translate(locale, "The Trending feed has been disabled by the administrator.") + message = translate(locale, "trending_page_disabled") templated "message" end end diff --git a/src/invidious/routes/search.cr b/src/invidious/routes/search.cr index eb253d2c..8a9a2d2b 100644 --- a/src/invidious/routes/search.cr +++ b/src/invidious/routes/search.cr @@ -42,7 +42,7 @@ module Invidious::Routes::Search # if search is disabled, show the “disabled” message immediately unless CONFIG.page_enabled?("search") - message = translate(locale, "Search has been disabled by the administrator.") + message = translate(locale, "search_page_disabled") return templated "message" end From e238624e8fdb21d4fd8894677adcc1b8507f9757 Mon Sep 17 00:00:00 2001 From: NorkzYT Date: Sat, 7 Jun 2025 11:52:13 +0000 Subject: [PATCH 17/33] feat(config.cr): introduce PagesEnabled struct for managing feature toggles for pages refactor(routes): replace direct page_enabled checks with centralized logic in before_all.cr for cleaner endpoint management chore(routes): remove redundant page_enabled checks from individual routes to streamline code and improve maintainability --- src/invidious/config.cr | 43 ++++++++++++++++++++++----- src/invidious/routes/api/v1/feeds.cr | 10 ------- src/invidious/routes/api/v1/search.cr | 5 ---- src/invidious/routes/before_all.cr | 22 ++++++++++++++ src/invidious/routes/feeds.cr | 34 +++++++-------------- src/invidious/routes/preferences.cr | 6 ++-- src/invidious/routes/search.cr | 6 ---- 7 files changed, 71 insertions(+), 55 deletions(-) diff --git a/src/invidious/config.cr b/src/invidious/config.cr index 36cb40a3..59380cb8 100644 --- a/src/invidious/config.cr +++ b/src/invidious/config.cr @@ -71,6 +71,37 @@ struct HTTPProxyConfig property port : Int32 end +# Structure used for global per-page feature toggles +struct PagesEnabled + include YAML::Serializable + + property trending : Bool = true + property popular : Bool = true + property search : Bool = true + + def has_key?(key : String) : Bool + %w(trending popular search).includes?(key) + end + + def [](key : String) : Bool + case key + when "trending" then @trending + when "popular" then @popular + when "search" then @search + else raise KeyError.new("Unknown page '#{key}'") + end + end + + def []=(key : String, value : Bool) + case key + when "trending" then @trending = value + when "popular" then @popular = value + when "search" then @search = value + else raise KeyError.new("Unknown page '#{key}'") + end + end +end + class Config include YAML::Serializable @@ -127,11 +158,7 @@ class Config # Global per-page feature toggles. # Valid keys: "trending", "popular", "search" # If someone sets both `popular_enabled` and `pages_enabled["popular"]`, the latter takes precedence. - property pages_enabled : Hash(String, Bool) = { - "trending" => true, - "popular" => true, - "search" => true, - } + property pages_enabled : PagesEnabled = PagesEnabled.new # ————————————————————————————————————————————————————————————————————————————————————— property captcha_enabled : Bool = true @@ -212,10 +239,10 @@ class Config # Centralized page toggle with legacy fallback for `popular_enabled` def page_enabled?(page : String) : Bool - if pages_enabled.has_key?(page) - pages_enabled[page] + if @pages_enabled.has_key?(page) + @pages_enabled[page] elsif page == "popular" - popular_enabled + @popular_enabled else true end diff --git a/src/invidious/routes/api/v1/feeds.cr b/src/invidious/routes/api/v1/feeds.cr index bfec4fb6..2e8328cb 100644 --- a/src/invidious/routes/api/v1/feeds.cr +++ b/src/invidious/routes/api/v1/feeds.cr @@ -4,11 +4,6 @@ module Invidious::Routes::API::V1::Feeds env.response.content_type = "application/json" - if !CONFIG.page_enabled?("trending") - error_message = {"error" => "Administrator has disabled this endpoint."}.to_json - haltf env, 403, error_message - end - region = env.params.query["region"]? trending_type = env.params.query["type"]? @@ -34,11 +29,6 @@ module Invidious::Routes::API::V1::Feeds env.response.content_type = "application/json" - if !CONFIG.page_enabled?("popular") - error_message = {"error" => "Administrator has disabled this endpoint."}.to_json - haltf env, 403, error_message - end - JSON.build do |json| json.array do popular_videos.each do |video| diff --git a/src/invidious/routes/api/v1/search.cr b/src/invidious/routes/api/v1/search.cr index 3d978c17..59a30745 100644 --- a/src/invidious/routes/api/v1/search.cr +++ b/src/invidious/routes/api/v1/search.cr @@ -5,11 +5,6 @@ module Invidious::Routes::API::V1::Search env.response.content_type = "application/json" - if !CONFIG.page_enabled?("search") - error_message = {"error" => "Administrator has disabled this endpoint."}.to_json - haltf env, 403, error_message - end - query = Invidious::Search::Query.new(env.params.query, :regular, region) begin diff --git a/src/invidious/routes/before_all.cr b/src/invidious/routes/before_all.cr index b5269668..5f6a1daa 100644 --- a/src/invidious/routes/before_all.cr +++ b/src/invidious/routes/before_all.cr @@ -102,6 +102,28 @@ module Invidious::Routes::BeforeAll preferences.locale = locale env.set "preferences", preferences + path = env.request.path + page_key = case path + when "/feed/popular", "/api/v1/popular" + "popular" + when "/feed/trending", "/api/v1/trending" + "trending" + when "/search", "/api/v1/search" + "search" + else + nil + end + + if page_key && !CONFIG.page_enabled?(page_key) + if path.starts_with?("/api/") + error_message = {error: "Administrator has disabled this endpoint."}.to_json + haltf env, 403, error_message + else + message = "#{page_key}_page_disabled" + return error_template(403, message) + end + end + # Allow media resources to be loaded from google servers # TODO: check if *.youtube.com can be removed # diff --git a/src/invidious/routes/feeds.cr b/src/invidious/routes/feeds.cr index c1852f5e..de82e1fb 100644 --- a/src/invidious/routes/feeds.cr +++ b/src/invidious/routes/feeds.cr @@ -33,36 +33,24 @@ module Invidious::Routes::Feeds def self.popular(env) locale = env.get("preferences").as(Preferences).locale - - if CONFIG.page_enabled?("popular") - templated "feeds/popular" - else - message = translate(locale, "popular_page_disabled") - templated "message" - end + templated "feeds/popular" end def self.trending(env) locale = env.get("preferences").as(Preferences).locale + trending_type = env.params.query["type"]? + trending_type ||= "Default" - if CONFIG.page_enabled?("trending") - trending_type = env.params.query["type"]? - trending_type ||= "Default" + region = env.params.query["region"]? + region ||= env.get("preferences").as(Preferences).region - region = env.params.query["region"]? - region ||= env.get("preferences").as(Preferences).region - - begin - trending, plid = fetch_trending(trending_type, region, locale) - rescue ex - return error_template(500, ex) - end - - templated "feeds/trending" - else - message = translate(locale, "trending_page_disabled") - templated "message" + begin + trending, plid = fetch_trending(trending_type, region, locale) + rescue ex + return error_template(500, ex) end + + templated "feeds/trending" end def self.subscriptions(env) diff --git a/src/invidious/routes/preferences.cr b/src/invidious/routes/preferences.cr index 34cea387..3852f8a9 100644 --- a/src/invidious/routes/preferences.cr +++ b/src/invidious/routes/preferences.cr @@ -200,9 +200,9 @@ module Invidious::Routes::PreferencesRoute CONFIG.default_user_preferences.feed_menu = admin_feed_menu pages_enabled = { - "popular" => (env.params.body["popular_enabled"]?.try &.as(String) || "off") == "on", - "trending" => (env.params.body["trending_enabled"]?.try &.as(String) || "off") == "on", - "search" => (env.params.body["search_enabled"]?.try &.as(String) || "off") == "on", + popular: (env.params.body["popular_enabled"]?.try &.as(String) || "on") == "on", + trending: (env.params.body["trending_enabled"]?.try &.as(String) || "on") == "on", + search: (env.params.body["search_enabled"]?.try &.as(String) || "on") == "on", } CONFIG.pages_enabled = pages_enabled diff --git a/src/invidious/routes/search.cr b/src/invidious/routes/search.cr index 8a9a2d2b..5b10887c 100644 --- a/src/invidious/routes/search.cr +++ b/src/invidious/routes/search.cr @@ -40,12 +40,6 @@ module Invidious::Routes::Search prefs = env.get("preferences").as(Preferences) locale = prefs.locale - # if search is disabled, show the “disabled” message immediately - unless CONFIG.page_enabled?("search") - message = translate(locale, "search_page_disabled") - return templated "message" - end - # otherwise, do a normal search region = env.params.query["region"]? || prefs.region query = Invidious::Search::Query.new(env.params.query, :regular, region) From ba65e4ff25f2dd4aaa02eefb1dd1a3a92e997190 Mon Sep 17 00:00:00 2001 From: syeopite Date: Sun, 24 Aug 2025 23:06:12 -0700 Subject: [PATCH 18/33] Config: Use from_yaml constructor for PagesEnabled `PagesEnabled` doesn't define a blank constructor without any parameters meaning that `PagesEnabled.new` leads to a error due to lack of arguments --- src/invidious/config.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/config.cr b/src/invidious/config.cr index 59380cb8..f9a3572a 100644 --- a/src/invidious/config.cr +++ b/src/invidious/config.cr @@ -158,7 +158,7 @@ class Config # Global per-page feature toggles. # Valid keys: "trending", "popular", "search" # If someone sets both `popular_enabled` and `pages_enabled["popular"]`, the latter takes precedence. - property pages_enabled : PagesEnabled = PagesEnabled.new + property pages_enabled : PagesEnabled = PagesEnabled.from_yaml("") # ————————————————————————————————————————————————————————————————————————————————————— property captcha_enabled : Bool = true From d496b6e34aaf03810853fcda081cbefd8f394bb5 Mon Sep 17 00:00:00 2001 From: syeopite Date: Sun, 24 Aug 2025 23:12:20 -0700 Subject: [PATCH 19/33] Use `PagesEnabled` struct when setting pages_enabled The config update logic for the admin panel was not updated to use the `PagesEnabled` struct introduced in commit `e238624` --- src/invidious/config.cr | 2 ++ src/invidious/routes/preferences.cr | 9 ++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/invidious/config.cr b/src/invidious/config.cr index f9a3572a..28b81b8d 100644 --- a/src/invidious/config.cr +++ b/src/invidious/config.cr @@ -79,6 +79,8 @@ struct PagesEnabled property popular : Bool = true property search : Bool = true + def initialize(@trending, @popular, @search); end + def has_key?(key : String) : Bool %w(trending popular search).includes?(key) end diff --git a/src/invidious/routes/preferences.cr b/src/invidious/routes/preferences.cr index 3852f8a9..d8317ae2 100644 --- a/src/invidious/routes/preferences.cr +++ b/src/invidious/routes/preferences.cr @@ -199,12 +199,11 @@ module Invidious::Routes::PreferencesRoute end CONFIG.default_user_preferences.feed_menu = admin_feed_menu - pages_enabled = { - popular: (env.params.body["popular_enabled"]?.try &.as(String) || "on") == "on", + CONFIG.pages_enabled = PagesEnabled.new( + popular: (env.params.body["popular_enabled"]?.try &.as(String) || "on") == "on", trending: (env.params.body["trending_enabled"]?.try &.as(String) || "on") == "on", - search: (env.params.body["search_enabled"]?.try &.as(String) || "on") == "on", - } - CONFIG.pages_enabled = pages_enabled + search: (env.params.body["search_enabled"]?.try &.as(String) || "on") == "on", + ) captcha_enabled = env.params.body["captcha_enabled"]?.try &.as(String) captcha_enabled ||= "off" From f978c2b228662ee1f6670da5dc4a88e68d2a0d7e Mon Sep 17 00:00:00 2001 From: syeopite Date: Mon, 25 Aug 2025 00:58:13 -0700 Subject: [PATCH 20/33] Fix config precedence with popular_enabled The original approach cannot be used to properly handle the precedence of `popular_enabled` in relations to the new `pages_enabled` since it is impossible to determine whether they are unset due to the presence of default values. In addition the original precedence handling wasn't actually doing anything since PagesEnabled.has_key? will always return true if the key is valid and thus use page_enabled and its default values regardless of whether popular_enabled was set or not (which again is also impossible to know) To fix this two new instance variables are introduced in order to track whether these two attributes were present in the YAML config and the logic for handling them is instead reduced to only using `page_enabled` and copying `popular_enabled` to page_enabled["popular"] when `page_enabled` field is unset. --- src/invidious/config.cr | 74 ++++++++++++++++++++++++++--------------- 1 file changed, 47 insertions(+), 27 deletions(-) diff --git a/src/invidious/config.cr b/src/invidious/config.cr index 28b81b8d..f218cb69 100644 --- a/src/invidious/config.cr +++ b/src/invidious/config.cr @@ -72,34 +72,26 @@ struct HTTPProxyConfig end # Structure used for global per-page feature toggles -struct PagesEnabled +record PagesEnabled, + trending : Bool = true, + popular : Bool = true, + search : Bool = true do include YAML::Serializable - property trending : Bool = true - property popular : Bool = true - property search : Bool = true - - def initialize(@trending, @popular, @search); end - - def has_key?(key : String) : Bool - %w(trending popular search).includes?(key) + def [](key : String) : Bool + fetch(key) { raise KeyError.new("Unknown page '#{key}'") } end - def [](key : String) : Bool + def []?(key : String) : Bool + fetch(key) { nil } + end + + private def fetch(key : String, &) case key when "trending" then @trending when "popular" then @popular when "search" then @search - else raise KeyError.new("Unknown page '#{key}'") - end - end - - def []=(key : String, value : Bool) - case key - when "trending" then @trending = value - when "popular" then @popular = value - when "search" then @search = value - else raise KeyError.new("Unknown page '#{key}'") + else yield end end end @@ -153,14 +145,26 @@ class Config property use_pubsub_feeds : Bool | Int32 = false # ————————————————————————————————————————————————————————————————————————————————————— + + # A @{{key}}_present variable is required for both fields in order to handle the precedence for + # the deprecated `popular_enabled` in relations to `pages_enabled` + # DEPRECATED: use `pages_enabled["popular"]` instead. @[Deprecated("`popular_enabled` will be removed in a future release; use pages_enabled[\"popular\"] instead")] + @[YAML::Field(presence: true)] property popular_enabled : Bool = true + @[YAML::Field(ignore: true)] + property popular_enabled_present : Bool + # Global per-page feature toggles. # Valid keys: "trending", "popular", "search" # If someone sets both `popular_enabled` and `pages_enabled["popular"]`, the latter takes precedence. + @[YAML::Field(presence: true)] property pages_enabled : PagesEnabled = PagesEnabled.from_yaml("") + + @[YAML::Field(ignore: true)] + property pages_enabled_present : Bool # ————————————————————————————————————————————————————————————————————————————————————— property captcha_enabled : Bool = true @@ -241,13 +245,7 @@ class Config # Centralized page toggle with legacy fallback for `popular_enabled` def page_enabled?(page : String) : Bool - if @pages_enabled.has_key?(page) - @pages_enabled[page] - elsif page == "popular" - @popular_enabled - else - true - end + return @pages_enabled[page] end def self.load @@ -336,6 +334,8 @@ class Config exit(1) end + config.process_deprecation + # Build database_url from db.* if it's not set directly if config.database_url.to_s.empty? if db = config.db @@ -373,4 +373,24 @@ class Config return config end + + # Processes deprecated values + # + # Warns when they are set and handles any precedence issue that may arise when present alongside a successor attribute + # + # This method is public as to allow specs to test the behavior without going through #load + # + # :nodoc: + def process_deprecation(log_io : IO = STDOUT) + # Handle deprecated popular_enabled config and warn if it is set + if self.popular_enabled_present + log_io.puts "Warning: `popular_enabled` has been deprecated and replaced by the `pages_enabled` config" + log_io.puts "If both are set `pages_enabled` will take precedence over `popular_enabled`" + + # Only use popular_enabled value when pages_enabled is unset + if !self.pages_enabled_present + self.pages_enabled = self.pages_enabled.copy_with(popular: self.popular_enabled) + end + end + end end From 245ffc8396b8cb03b7fb163f3d9fafa4818dac5a Mon Sep 17 00:00:00 2001 From: syeopite Date: Mon, 25 Aug 2025 01:35:40 -0700 Subject: [PATCH 21/33] Mark attributes set over env var as present if needed --- src/invidious/config.cr | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/invidious/config.cr b/src/invidious/config.cr index f218cb69..848a8a95 100644 --- a/src/invidious/config.cr +++ b/src/invidious/config.cr @@ -285,6 +285,12 @@ class Config begin config.{{ivar.id}} = ivar_type.from_yaml(env_value) success = true + + # Update associated _present key if any + {% other_ivar = @type.instance_vars.find { |other_ivar| other_ivar.name == ivar.name + "_present" } %} + {% if other_ivar && (ann = other_ivar.annotation(YAML::Field)) && ann[:ignore] == true %} + config.{{other_ivar.name.id}} = true + {% end %} rescue # nop end From 20e4e52b8b35a9df996a6047483839c27f0fa607 Mon Sep 17 00:00:00 2001 From: syeopite Date: Mon, 25 Aug 2025 01:36:05 -0700 Subject: [PATCH 22/33] Add tests for `popular_enabled` deprecation logic --- spec/invidious/config_spec.cr | 50 +++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 spec/invidious/config_spec.cr diff --git a/spec/invidious/config_spec.cr b/spec/invidious/config_spec.cr new file mode 100644 index 00000000..1e9d3355 --- /dev/null +++ b/spec/invidious/config_spec.cr @@ -0,0 +1,50 @@ +require "../spec_helper" +require "../../src/invidious/jobs.cr" +require "../../src/invidious/jobs/*" +require "../../src/invidious/config.cr" +require "../../src/invidious/user/preferences.cr" + +# Allow this file to be executed independently of other specs +{% if !@type.has_constant?("CONFIG") %} + CONFIG = Config.from_yaml("") +{% end %} + +private def construct_config(yaml) + config = Config.from_yaml(yaml) + File.open(File::NULL, "w") { |io| config.process_deprecation(io) } + return config +end + +Spectator.describe Config do + context "page_enabled" do + it "Can disable pages" do + config = construct_config <<-YAML + pages_enabled: + popular: false + search: false + YAML + + expect(config.page_enabled?("trending")).to eq(true) + expect(config.page_enabled?("popular")).to eq(false) + expect(config.page_enabled?("search")).to eq(false) + end + + it "Takes precedence over popular_enabled" do + config = construct_config <<-YAML + popular_enabled: false + pages_enabled: + popular: true + YAML + + expect(config.page_enabled?("popular")).to eq(true) + end + end + + it "Deprecated popular_enabled still works" do + config = construct_config <<-YAML + popular_enabled: false + YAML + + expect(config.page_enabled?("popular")).to eq(false) + end +end From 24d0724ff9ff72b3c51c06bf4831182fe7fc3d51 Mon Sep 17 00:00:00 2001 From: NorkzYT Date: Tue, 28 Oct 2025 17:53:19 -0400 Subject: [PATCH 23/33] chore: disable trending by default --- config/config.example.yml | 2 +- spec/invidious/config_spec.cr | 2 +- src/invidious/config.cr | 2 +- src/invidious/views/components/feed_menu.ecr | 12 ++++++++++++ 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/config/config.example.yml b/config/config.example.yml index 64fc970c..d51223d3 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -314,7 +314,7 @@ https_only: false ## Enable/Disable specific pages on the main page. ## #pages_enabled: -# trending: true +# trending: false # popular: true # search: true ## diff --git a/spec/invidious/config_spec.cr b/spec/invidious/config_spec.cr index 1e9d3355..50465851 100644 --- a/spec/invidious/config_spec.cr +++ b/spec/invidious/config_spec.cr @@ -24,7 +24,7 @@ Spectator.describe Config do search: false YAML - expect(config.page_enabled?("trending")).to eq(true) + expect(config.page_enabled?("trending")).to eq(false) expect(config.page_enabled?("popular")).to eq(false) expect(config.page_enabled?("search")).to eq(false) end diff --git a/src/invidious/config.cr b/src/invidious/config.cr index 848a8a95..b274b75f 100644 --- a/src/invidious/config.cr +++ b/src/invidious/config.cr @@ -73,7 +73,7 @@ end # Structure used for global per-page feature toggles record PagesEnabled, - trending : Bool = true, + trending : Bool = false, popular : Bool = true, search : Bool = true do include YAML::Serializable diff --git a/src/invidious/views/components/feed_menu.ecr b/src/invidious/views/components/feed_menu.ecr index 3dbeaf37..b8e60e3a 100644 --- a/src/invidious/views/components/feed_menu.ecr +++ b/src/invidious/views/components/feed_menu.ecr @@ -3,6 +3,18 @@ <% if !env.get?("user") %> <% feed_menu.reject! {|item| {"Subscriptions", "Playlists"}.includes? item} %> <% end %> + <% feed_menu.reject! do |feed| + case feed + when "Popular" + !CONFIG.page_enabled?("popular") + when "Trending" + !CONFIG.page_enabled?("trending") + when "" + !CONFIG.page_enabled?("search") + else + false + end + end %> <% feed_menu.each do |feed| %> <%= translate(locale, feed) %> From dee1bd61dbf18518b8f2b5d453bfaf85d7e2ed6b Mon Sep 17 00:00:00 2001 From: NorkzYT Date: Tue, 28 Oct 2025 18:28:01 -0400 Subject: [PATCH 24/33] chore: missing hash key issue --- src/invidious/routes/before_all.cr | 57 +++++++++++++++--------------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/src/invidious/routes/before_all.cr b/src/invidious/routes/before_all.cr index 5f6a1daa..275a4396 100644 --- a/src/invidious/routes/before_all.cr +++ b/src/invidious/routes/before_all.cr @@ -103,6 +103,35 @@ module Invidious::Routes::BeforeAll env.set "preferences", preferences path = env.request.path + + # Allow media resources to be loaded from google servers + # TODO: check if *.youtube.com can be removed + # + # `!preferences.local` has to be checked after setting and + # reading `preferences` from the "PREFS" cookie and + # saved user preferences from the database, otherwise + # `https://*.googlevideo.com:443 https://*.youtube.com:443` + # will not be set in the CSP header if + # `default_user_preferences.local` is set to true on the + # configuration file, causing preference “Proxy Videos” + # not to work while having it disabled and using medium quality. + if CONFIG.disabled?("local") || !preferences.local + env.response.headers["Content-Security-Policy"] = env.response.headers["Content-Security-Policy"].gsub("media-src", "media-src https://*.googlevideo.com:443 https://*.youtube.com:443") + end + + current_page = path + if env.request.query + query = HTTP::Params.parse(env.request.query.not_nil!) + + if query["referer"]? + query["referer"] = get_referer(env, "/") + end + + current_page += "?#{query}" + end + + env.set "current_page", URI.encode_www_form(current_page) + page_key = case path when "/feed/popular", "/api/v1/popular" "popular" @@ -123,33 +152,5 @@ module Invidious::Routes::BeforeAll return error_template(403, message) end end - - # Allow media resources to be loaded from google servers - # TODO: check if *.youtube.com can be removed - # - # `!preferences.local` has to be checked after setting and - # reading `preferences` from the "PREFS" cookie and - # saved user preferences from the database, otherwise - # `https://*.googlevideo.com:443 https://*.youtube.com:443` - # will not be set in the CSP header if - # `default_user_preferences.local` is set to true on the - # configuration file, causing preference “Proxy Videos” - # not to work while having it disabled and using medium quality. - if CONFIG.disabled?("local") || !preferences.local - env.response.headers["Content-Security-Policy"] = env.response.headers["Content-Security-Policy"].gsub("media-src", "media-src https://*.googlevideo.com:443 https://*.youtube.com:443") - end - - current_page = env.request.path - if env.request.query - query = HTTP::Params.parse(env.request.query.not_nil!) - - if query["referer"]? - query["referer"] = get_referer(env, "/") - end - - current_page += "?#{query}" - end - - env.set "current_page", URI.encode_www_form(current_page) end end From ef5931273158bdb1ee8a97aad905451f1277ab71 Mon Sep 17 00:00:00 2001 From: NorkzYT Date: Sun, 16 Nov 2025 13:36:37 +0000 Subject: [PATCH 25/33] latest from master --- docker-compose.yml | 89 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 80 insertions(+), 9 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index afda8726..3ab4a818 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,16 +4,17 @@ # If you want to use Invidious in production, see the docker-compose.yml file provided # in the installation documentation: https://docs.invidious.io/installation/ -version: "3" services: - invidious: + container_name: invidious + hostname: invidious + # image: quay.io/invidious/invidious:latest build: context: . dockerfile: docker/Dockerfile restart: unless-stopped ports: - - "127.0.0.1:3000:3000" + - "3000:3000" environment: # Please read the following file for a comprehensive list of all available # configuration options and their associated syntax: @@ -26,19 +27,81 @@ services: host: invidious-db port: 5432 check_tables: true - # external_port: - # domain: - # https_only: false - # statistics_enabled: false - hmac_key: "CHANGE_ME!!" + + # IT is NOT recommended to use the same key as HMAC KEY. Generate a new key! + # Use the key generated in the 2nd step + invidious_companion_key: ksMXJJmbDiZLBmw8 + # URL used for the internal communication between invidious and invidious companion + # There is no need to change that except if Invidious companion does not run on the same docker compose file. + invidious_companion: + - private_url: "http://companion:8282/companion" + + # Reverse-proxy awareness + external_port: 443 + domain: invidious-miraidon.pcscorp.dev + https_only: true + + # Server-wide flags (real, upstream-supported) + hmac_key: "gf35h462Fe24g4tu46beg4" + registration_enabled: true + captcha_enabled: false + admins: ["norkz"] + + pages_enabled: + popular: false + trending: false + search: false + + # ↓ These MUST be nested here, not at top level + default_user_preferences: + player_style: youtube + save_player_pos: true + # Hide Popular/Trending from the menu by not listing them + feed_menu: ["Subscriptions", "Playlists"] + # Use literal string "" to show no feed by default + default_home: "" + related_videos: false healthcheck: - test: wget -nv --tries=1 --spider http://127.0.0.1:3000/api/v1/trending || exit 1 + test: wget -nv --tries=1 --spider http://127.0.0.1:3000/ || exit 1 interval: 30s timeout: 5s retries: 2 + depends_on: + invidious-db: + condition: service_healthy + networks: + - proxy + + companion: + image: quay.io/invidious/invidious-companion:latest + environment: + # Use the key generated in the 2nd step + - SERVER_SECRET_KEY=ksMXJJmbDiZLBmw8 + restart: unless-stopped + # Uncomment only if you have configured "public_url" for Invidious companion + # Or if you want to use Invidious companion as an API in your program. + # Remove "127.0.0.1:" if used from an external IP + ports: + - 8282:8282 + logging: + options: + max-size: "1G" + max-file: "4" + cap_drop: + - ALL + read_only: true + # cache for youtube library + volumes: + - companioncache:/var/tmp/youtubei.js:rw + security_opt: + - no-new-privileges:true + networks: + - proxy invidious-db: image: docker.io/library/postgres:14 + container_name: invidious-db + hostname: invidious-db restart: unless-stopped volumes: - postgresdata:/var/lib/postgresql/data @@ -50,6 +113,14 @@ services: POSTGRES_PASSWORD: kemal healthcheck: test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] + networks: + - proxy volumes: postgresdata: + companioncache: + +networks: + proxy: + driver: bridge + external: true From 8c922d05c1b48dce374f4aa1e0419df2d927df23 Mon Sep 17 00:00:00 2001 From: NorkzYT Date: Sun, 16 Nov 2025 08:41:49 -0500 Subject: [PATCH 26/33] revert docker compose file --- docker-compose.yml | 91 ++++------------------------------------------ 1 file changed, 8 insertions(+), 83 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 814f2918..afda8726 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,21 +4,16 @@ # If you want to use Invidious in production, see the docker-compose.yml file provided # in the installation documentation: https://docs.invidious.io/installation/ +version: "3" services: + invidious: - container_name: invidious - hostname: invidious - # image: quay.io/invidious/invidious:latest build: context: . dockerfile: docker/Dockerfile restart: unless-stopped ports: - "127.0.0.1:3000:3000" - depends_on: - invidious-db: - condition: service_healthy - restart: true environment: # Please read the following file for a comprehensive list of all available # configuration options and their associated syntax: @@ -31,81 +26,19 @@ services: host: invidious-db port: 5432 check_tables: true - - # IT is NOT recommended to use the same key as HMAC KEY. Generate a new key! - # Use the key generated in the 2nd step - invidious_companion_key: ksMXJJmbDiZLBmw8 - # URL used for the internal communication between invidious and invidious companion - # There is no need to change that except if Invidious companion does not run on the same docker compose file. - invidious_companion: - - private_url: "http://companion:8282/companion" - - # Reverse-proxy awareness - external_port: 443 - domain: invidious-miraidon.pcscorp.dev - https_only: true - - # Server-wide flags (real, upstream-supported) - hmac_key: "gf35h462Fe24g4tu46beg4" - registration_enabled: true - captcha_enabled: false - admins: ["norkz"] - - pages_enabled: - popular: false - trending: false - search: false - - # ↓ These MUST be nested here, not at top level - default_user_preferences: - player_style: youtube - save_player_pos: true - # Hide Popular/Trending from the menu by not listing them - feed_menu: ["Subscriptions", "Playlists"] - # Use literal string "" to show no feed by default - default_home: "" - related_videos: false + # external_port: + # domain: + # https_only: false + # statistics_enabled: false + hmac_key: "CHANGE_ME!!" healthcheck: - test: wget -nv --tries=1 --spider http://127.0.0.1:3000/ || exit 1 + test: wget -nv --tries=1 --spider http://127.0.0.1:3000/api/v1/trending || exit 1 interval: 30s timeout: 5s retries: 2 - depends_on: - invidious-db: - condition: service_healthy - networks: - - proxy - - companion: - image: quay.io/invidious/invidious-companion:latest - environment: - # Use the key generated in the 2nd step - - SERVER_SECRET_KEY=ksMXJJmbDiZLBmw8 - restart: unless-stopped - # Uncomment only if you have configured "public_url" for Invidious companion - # Or if you want to use Invidious companion as an API in your program. - # Remove "127.0.0.1:" if used from an external IP - ports: - - 8282:8282 - logging: - options: - max-size: "1G" - max-file: "4" - cap_drop: - - ALL - read_only: true - # cache for youtube library - volumes: - - companioncache:/var/tmp/youtubei.js:rw - security_opt: - - no-new-privileges:true - networks: - - proxy invidious-db: image: docker.io/library/postgres:14 - container_name: invidious-db - hostname: invidious-db restart: unless-stopped volumes: - postgresdata:/var/lib/postgresql/data @@ -117,14 +50,6 @@ services: POSTGRES_PASSWORD: kemal healthcheck: test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] - networks: - - proxy volumes: postgresdata: - companioncache: - -networks: - proxy: - driver: bridge - external: true From bd6dd9cb5592c4de899000214c04fa7e4cf1a416 Mon Sep 17 00:00:00 2001 From: Richard Lora Date: Sat, 28 Feb 2026 19:01:35 -0500 Subject: [PATCH 27/33] feat: add search_page_disabled locale + preferences page filtering + debug config route - Fix kemal_static_file_handler directory_listing to use Path types (Crystal API compat) - Add missing search_page_disabled translation key to en-US.json - Filter preferences default_home options based on pages_enabled config - Add admin-only /debug_config route for verifying configuration state --- locales/en-US.json | 1023 +++++++++++----------- src/ext/kemal_static_file_handler.cr | 2 +- src/invidious/routes/debug_config.cr | 114 +++ src/invidious/views/user/preferences.ecr | 19 +- 4 files changed, 641 insertions(+), 517 deletions(-) create mode 100644 src/invidious/routes/debug_config.cr diff --git a/locales/en-US.json b/locales/en-US.json index 1530e1ba..738af1d4 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -1,513 +1,514 @@ { - "Add to playlist": "Add to playlist", - "Add to playlist: ": "Add to playlist: ", - "Answer": "Answer", - "Search for videos": "Search for videos", - "popular_page_disabled": "The Popular feed has been disabled by the administrator.", - "trending_page_disabled": "The Trending feed has been disabled by the administrator.", - "generic_channels_count": "{{count}} channel", - "generic_channels_count_plural": "{{count}} channels", - "generic_views_count": "{{count}} view", - "generic_views_count_plural": "{{count}} views", - "generic_videos_count": "{{count}} video", - "generic_videos_count_plural": "{{count}} videos", - "generic_playlists_count": "{{count}} playlist", - "generic_playlists_count_plural": "{{count}} playlists", - "generic_subscribers_count": "{{count}} subscriber", - "generic_subscribers_count_plural": "{{count}} subscribers", - "generic_subscriptions_count": "{{count}} subscription", - "generic_subscriptions_count_plural": "{{count}} subscriptions", - "generic_button_delete": "Delete", - "generic_button_edit": "Edit", - "generic_button_save": "Save", - "generic_button_cancel": "Cancel", - "generic_button_rss": "RSS", - "LIVE": "LIVE", - "Shared `x` ago": "Shared `x` ago", - "Unsubscribe": "Unsubscribe", - "Subscribe": "Subscribe", - "View channel on YouTube": "View channel on YouTube", - "View playlist on YouTube": "View playlist on YouTube", - "newest": "newest", - "oldest": "oldest", - "popular": "popular", - "last": "last", - "Next page": "Next page", - "Previous page": "Previous page", - "First page": "First page", - "Clear watch history?": "Clear watch history?", - "New password": "New password", - "New passwords must match": "New passwords must match", - "Authorize token?": "Authorize token?", - "Authorize token for `x`?": "Authorize token for `x`?", - "Yes": "Yes", - "No": "No", - "Import and Export Data": "Import and Export Data", - "Import": "Import", - "Import Invidious data": "Import Invidious JSON data", - "Import YouTube subscriptions": "Import YouTube CSV or OPML subscriptions", - "Import YouTube playlist (.csv)": "Import YouTube playlist (.csv)", - "Import YouTube watch history (.json)": "Import YouTube watch history (.json)", - "Import FreeTube subscriptions (.db)": "Import FreeTube subscriptions (.db)", - "Import NewPipe subscriptions (.json)": "Import NewPipe subscriptions (.json)", - "Import NewPipe data (.zip)": "Import NewPipe data (.zip)", - "Export": "Export", - "Export subscriptions as OPML": "Export subscriptions as OPML", - "Export subscriptions as OPML (for NewPipe & FreeTube)": "Export subscriptions as OPML (for NewPipe & FreeTube)", - "Export data as JSON": "Export Invidious data as JSON", - "Delete account?": "Delete account?", - "History": "History", - "An alternative front-end to YouTube": "An alternative front-end to YouTube", - "JavaScript license information": "JavaScript license information", - "source": "source", - "Log in": "Log in", - "Log in/register": "Log in/register", - "User ID": "User ID", - "Password": "Password", - "Time (h:mm:ss):": "Time (h:mm:ss):", - "Sign In": "Sign In", - "Register": "Register", - "E-mail": "E-mail", - "Preferences": "Preferences", - "preferences_category_player": "Player preferences", - "preferences_video_loop_label": "Always loop: ", - "preferences_preload_label": "Preload video data: ", - "preferences_autoplay_label": "Autoplay: ", - "preferences_continue_label": "Play next by default: ", - "preferences_continue_autoplay_label": "Autoplay next video: ", - "preferences_listen_label": "Listen by default: ", - "preferences_local_label": "Proxy videos: ", - "preferences_watch_history_label": "Enable watch history: ", - "preferences_speed_label": "Default speed: ", - "preferences_quality_label": "Preferred video quality: ", - "preferences_quality_option_dash": "DASH (adaptive quality)", - "preferences_quality_option_hd720": "HD720", - "preferences_quality_option_medium": "Medium", - "preferences_quality_option_small": "Small", - "preferences_quality_dash_label": "Preferred DASH video quality: ", - "preferences_quality_dash_option_auto": "Auto", - "preferences_quality_dash_option_best": "Best", - "preferences_quality_dash_option_worst": "Worst", - "preferences_quality_dash_option_4320p": "4320p", - "preferences_quality_dash_option_2160p": "2160p", - "preferences_quality_dash_option_1440p": "1440p", - "preferences_quality_dash_option_1080p": "1080p", - "preferences_quality_dash_option_720p": "720p", - "preferences_quality_dash_option_480p": "480p", - "preferences_quality_dash_option_360p": "360p", - "preferences_quality_dash_option_240p": "240p", - "preferences_quality_dash_option_144p": "144p", - "preferences_volume_label": "Player volume: ", - "preferences_comments_label": "Default comments: ", - "youtube": "YouTube", - "reddit": "Reddit", - "invidious": "Invidious", - "preferences_captions_label": "Default captions: ", - "Fallback captions: ": "Fallback captions: ", - "preferences_related_videos_label": "Show related videos: ", - "preferences_annotations_label": "Show annotations by default: ", - "preferences_extend_desc_label": "Automatically extend video description: ", - "preferences_vr_mode_label": "Interactive 360 degree videos (requires WebGL): ", - "preferences_category_visual": "Visual preferences", - "preferences_region_label": "Content country: ", - "preferences_player_style_label": "Player style: ", - "Dark mode: ": "Dark mode: ", - "preferences_dark_mode_label": "Theme: ", - "dark": "dark", - "light": "light", - "preferences_thin_mode_label": "Thin mode: ", - "preferences_category_misc": "Miscellaneous preferences", - "preferences_automatic_instance_redirect_label": "Automatic instance redirection (fallback to redirect.invidious.io): ", - "preferences_category_subscription": "Subscription preferences", - "preferences_annotations_subscribed_label": "Show annotations by default for subscribed channels? ", - "Redirect homepage to feed: ": "Redirect homepage to feed: ", - "preferences_max_results_label": "Number of videos shown in feed: ", - "preferences_sort_label": "Sort videos by: ", - "preferences_default_playlist": "Default playlist: ", - "preferences_default_playlist_none": "No default playlist set", - "published": "published", - "published - reverse": "published - reverse", - "alphabetically": "alphabetically", - "alphabetically - reverse": "alphabetically - reverse", - "channel name": "channel name", - "channel name - reverse": "channel name - reverse", - "Only show latest video from channel: ": "Only show latest video from channel: ", - "Only show latest unwatched video from channel: ": "Only show latest unwatched video from channel: ", - "preferences_unseen_only_label": "Only show unwatched: ", - "preferences_notifications_only_label": "Only show notifications (if there are any): ", - "Enable web notifications": "Enable web notifications", - "`x` uploaded a video": "`x` uploaded a video", - "`x` is live": "`x` is live", - "preferences_category_data": "Data preferences", - "Clear watch history": "Clear watch history", - "Import/export data": "Import/export data", - "Change password": "Change password", - "Manage subscriptions": "Manage subscriptions", - "Manage tokens": "Manage tokens", - "Watch history": "Watch history", - "Delete account": "Delete account", - "preferences_category_admin": "Administrator preferences", - "preferences_default_home_label": "Default homepage: ", - "preferences_feed_menu_label": "Feed menu: ", - "preferences_show_nick_label": "Show nickname on top: ", - "Popular enabled: ": "Popular enabled: ", - "Top enabled: ": "Top enabled: ", - "CAPTCHA enabled: ": "CAPTCHA enabled: ", - "Login enabled: ": "Login enabled: ", - "Registration enabled: ": "Registration enabled: ", - "Report statistics: ": "Report statistics: ", - "Save preferences": "Save preferences", - "Subscription manager": "Subscription manager", - "Token manager": "Token manager", - "Token": "Token", - "tokens_count": "{{count}} token", - "tokens_count_plural": "{{count}} tokens", - "Import/export": "Import/export", - "unsubscribe": "unsubscribe", - "revoke": "revoke", - "Subscriptions": "Subscriptions", - "subscriptions_unseen_notifs_count": "{{count}} unseen notification", - "subscriptions_unseen_notifs_count_plural": "{{count}} unseen notifications", - "search": "search", - "Log out": "Log out", - "Released under the AGPLv3 on Github.": "Released under the AGPLv3 on GitHub.", - "Source available here.": "Source available here.", - "View JavaScript license information.": "View JavaScript license information.", - "View privacy policy.": "View privacy policy.", - "Trending": "Trending", - "Public": "Public", - "Unlisted": "Unlisted", - "Private": "Private", - "View all playlists": "View all playlists", - "Updated `x` ago": "Updated `x` ago", - "Delete playlist `x`?": "Delete playlist `x`?", - "Delete playlist": "Delete playlist", - "Create playlist": "Create playlist", - "Title": "Title", - "Playlist privacy": "Playlist privacy", - "Editing playlist `x`": "Editing playlist `x`", - "playlist_button_add_items": "Add videos", - "Show more": "Show more", - "Show less": "Show less", - "Watch on YouTube": "Watch on YouTube", - "Switch Invidious Instance": "Switch Invidious Instance", - "search_message_no_results": "No results found.", - "search_message_change_filters_or_query": "Try widening your search query and/or changing the filters.", - "search_message_use_another_instance": "You can also search on another instance.", - "Hide annotations": "Hide annotations", - "Show annotations": "Show annotations", - "Genre: ": "Genre: ", - "License: ": "License: ", - "Standard YouTube license": "Standard YouTube license", - "Family friendly? ": "Family friendly? ", - "Wilson score: ": "Wilson score: ", - "Engagement: ": "Engagement: ", - "Whitelisted regions: ": "Whitelisted regions: ", - "Blacklisted regions: ": "Blacklisted regions: ", - "Music in this video": "Music in this video", - "Artist: ": "Artist: ", - "Song: ": "Song: ", - "Album: ": "Album: ", - "Shared `x`": "Shared `x`", - "Premieres in `x`": "Premieres in `x`", - "Premieres `x`": "Premieres `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.": "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.", - "View YouTube comments": "View YouTube comments", - "View more comments on Reddit": "View more comments on Reddit", - "View `x` comments": { - "([^.,0-9]|^)1([^.,0-9]|$)": "View `x` comment", - "": "View `x` comments" - }, - "View Reddit comments": "View Reddit comments", - "Hide replies": "Hide replies", - "Show replies": "Show replies", - "Incorrect password": "Incorrect password", - "Wrong answer": "Wrong answer", - "Erroneous CAPTCHA": "Erroneous CAPTCHA", - "CAPTCHA is a required field": "CAPTCHA is a required field", - "User ID is a required field": "User ID is a required field", - "Password is a required field": "Password is a required field", - "Wrong username or password": "Wrong username or password", - "Password cannot be empty": "Password cannot be empty", - "Password cannot be longer than 55 characters": "Password cannot be longer than 55 characters", - "Please log in": "Please log in", - "Invidious Private Feed for `x`": "Invidious Private Feed for `x`", - "channel:`x`": "channel:`x`", - "Deleted or invalid channel": "Deleted or invalid channel", - "This channel does not exist.": "This channel does not exist.", - "Could not get channel info.": "Could not get channel info.", - "Could not fetch comments": "Could not fetch comments", - "comments_view_x_replies": "View {{count}} reply", - "comments_view_x_replies_plural": "View {{count}} replies", - "`x` ago": "`x` ago", - "Load more": "Load more", - "comments_points_count": "{{count}} point", - "comments_points_count_plural": "{{count}} points", - "Could not create mix.": "Could not create mix.", - "Empty playlist": "Empty playlist", - "Not a playlist.": "Not a playlist.", - "Playlist does not exist.": "Playlist does not exist.", - "Could not pull trending pages.": "Could not pull trending pages.", - "Hidden field \"challenge\" is a required field": "Hidden field \"challenge\" is a required field", - "Hidden field \"token\" is a required field": "Hidden field \"token\" is a required field", - "Erroneous challenge": "Erroneous challenge", - "Erroneous token": "Erroneous token", - "No such user": "No such user", - "Token is expired, please try again": "Token is expired, please try again", - "English": "English", - "English (United Kingdom)": "English (United Kingdom)", - "English (United States)": "English (United States)", - "English (auto-generated)": "English (auto-generated)", - "Afrikaans": "Afrikaans", - "Albanian": "Albanian", - "Amharic": "Amharic", - "Arabic": "Arabic", - "Armenian": "Armenian", - "Azerbaijani": "Azerbaijani", - "Bangla": "Bangla", - "Basque": "Basque", - "Belarusian": "Belarusian", - "Bosnian": "Bosnian", - "Bulgarian": "Bulgarian", - "Burmese": "Burmese", - "Cantonese (Hong Kong)": "Cantonese (Hong Kong)", - "Catalan": "Catalan", - "Cebuano": "Cebuano", - "Chinese": "Chinese", - "Chinese (China)": "Chinese (China)", - "Chinese (Hong Kong)": "Chinese (Hong Kong)", - "Chinese (Simplified)": "Chinese (Simplified)", - "Chinese (Taiwan)": "Chinese (Taiwan)", - "Chinese (Traditional)": "Chinese (Traditional)", - "Corsican": "Corsican", - "Croatian": "Croatian", - "Czech": "Czech", - "Danish": "Danish", - "Dutch": "Dutch", - "Dutch (auto-generated)": "Dutch (auto-generated)", - "Esperanto": "Esperanto", - "Estonian": "Estonian", - "Filipino": "Filipino", - "Filipino (auto-generated)": "Filipino (auto-generated)", - "Finnish": "Finnish", - "French": "French", - "French (auto-generated)": "French (auto-generated)", - "Galician": "Galician", - "Georgian": "Georgian", - "German": "German", - "German (auto-generated)": "German (auto-generated)", - "Greek": "Greek", - "Gujarati": "Gujarati", - "Haitian Creole": "Haitian Creole", - "Hausa": "Hausa", - "Hawaiian": "Hawaiian", - "Hebrew": "Hebrew", - "Hindi": "Hindi", - "Hmong": "Hmong", - "Hungarian": "Hungarian", - "Icelandic": "Icelandic", - "Igbo": "Igbo", - "Indonesian": "Indonesian", - "Indonesian (auto-generated)": "Indonesian (auto-generated)", - "Interlingue": "Interlingue", - "Irish": "Irish", - "Italian": "Italian", - "Italian (auto-generated)": "Italian (auto-generated)", - "Japanese": "Japanese", - "Japanese (auto-generated)": "Japanese (auto-generated)", - "Javanese": "Javanese", - "Kannada": "Kannada", - "Kazakh": "Kazakh", - "Khmer": "Khmer", - "Korean": "Korean", - "Korean (auto-generated)": "Korean (auto-generated)", - "Kurdish": "Kurdish", - "Kyrgyz": "Kyrgyz", - "Lao": "Lao", - "Latin": "Latin", - "Latvian": "Latvian", - "Lithuanian": "Lithuanian", - "Luxembourgish": "Luxembourgish", - "Macedonian": "Macedonian", - "Malagasy": "Malagasy", - "Malay": "Malay", - "Malayalam": "Malayalam", - "Maltese": "Maltese", - "Maori": "Maori", - "Marathi": "Marathi", - "Mongolian": "Mongolian", - "Nepali": "Nepali", - "Norwegian Bokmål": "Norwegian Bokmål", - "Nyanja": "Nyanja", - "Pashto": "Pashto", - "Persian": "Persian", - "Polish": "Polish", - "Portuguese": "Portuguese", - "Portuguese (auto-generated)": "Portuguese (auto-generated)", - "Portuguese (Brazil)": "Portuguese (Brazil)", - "Punjabi": "Punjabi", - "Romanian": "Romanian", - "Russian": "Russian", - "Russian (auto-generated)": "Russian (auto-generated)", - "Samoan": "Samoan", - "Scottish Gaelic": "Scottish Gaelic", - "Serbian": "Serbian", - "Shona": "Shona", - "Sindhi": "Sindhi", - "Sinhala": "Sinhala", - "Slovak": "Slovak", - "Slovenian": "Slovenian", - "Somali": "Somali", - "Southern Sotho": "Southern Sotho", - "Spanish": "Spanish", - "Spanish (auto-generated)": "Spanish (auto-generated)", - "Spanish (Latin America)": "Spanish (Latin America)", - "Spanish (Mexico)": "Spanish (Mexico)", - "Spanish (Spain)": "Spanish (Spain)", - "Sundanese": "Sundanese", - "Swahili": "Swahili", - "Swedish": "Swedish", - "Tajik": "Tajik", - "Tamil": "Tamil", - "Telugu": "Telugu", - "Thai": "Thai", - "Turkish": "Turkish", - "Turkish (auto-generated)": "Turkish (auto-generated)", - "Ukrainian": "Ukrainian", - "Urdu": "Urdu", - "Uzbek": "Uzbek", - "Vietnamese": "Vietnamese", - "Vietnamese (auto-generated)": "Vietnamese (auto-generated)", - "Welsh": "Welsh", - "Western Frisian": "Western Frisian", - "Xhosa": "Xhosa", - "Yiddish": "Yiddish", - "Yoruba": "Yoruba", - "Zulu": "Zulu", - "generic_count_years": "{{count}} year", - "generic_count_years_plural": "{{count}} years", - "generic_count_months": "{{count}} month", - "generic_count_months_plural": "{{count}} months", - "generic_count_weeks": "{{count}} week", - "generic_count_weeks_plural": "{{count}} weeks", - "generic_count_days": "{{count}} day", - "generic_count_days_plural": "{{count}} days", - "generic_count_hours": "{{count}} hour", - "generic_count_hours_plural": "{{count}} hours", - "generic_count_minutes": "{{count}} minute", - "generic_count_minutes_plural": "{{count}} minutes", - "generic_count_seconds": "{{count}} second", - "generic_count_seconds_plural": "{{count}} seconds", - "Fallback comments: ": "Fallback comments: ", - "Popular": "Popular", - "Search": "Search", - "Top": "Top", - "About": "About", - "Rating: ": "Rating: ", - "preferences_locale_label": "Language: ", - "View as playlist": "View as playlist", - "Default": "Default", - "Music": "Music", - "Gaming": "Gaming", - "Livestreams": "Livestreams", - "News": "News", - "Movies": "Movies", - "Download": "Download", - "Download as: ": "Download as: ", - "Download is disabled": "Download is disabled", - "%A %B %-d, %Y": "%A %B %-d, %Y", - "(edited)": "(edited)", - "YouTube comment permalink": "YouTube comment permalink", - "permalink": "permalink", - "`x` marked it with a ❤": "`x` marked it with a ❤", - "Channel Sponsor": "Channel Sponsor", - "Audio mode": "Audio mode", - "Video mode": "Video mode", - "Playlists": "Playlists", - "search_filters_title": "Filters", - "search_filters_date_label": "Upload date", - "search_filters_date_option_none": "Any date", - "search_filters_date_option_hour": "Last hour", - "search_filters_date_option_today": "Today", - "search_filters_date_option_week": "This week", - "search_filters_date_option_month": "This month", - "search_filters_date_option_year": "This year", - "search_filters_type_label": "Type", - "search_filters_type_option_all": "Any type", - "search_filters_type_option_video": "Video", - "search_filters_type_option_channel": "Channel", - "search_filters_type_option_playlist": "Playlist", - "search_filters_type_option_movie": "Movie", - "search_filters_type_option_show": "Show", - "search_filters_duration_label": "Duration", - "search_filters_duration_option_none": "Any duration", - "search_filters_duration_option_short": "Short (< 4 minutes)", - "search_filters_duration_option_medium": "Medium (4 - 20 minutes)", - "search_filters_duration_option_long": "Long (> 20 minutes)", - "search_filters_features_label": "Features", - "search_filters_features_option_live": "Live", - "search_filters_features_option_four_k": "4K", - "search_filters_features_option_hd": "HD", - "search_filters_features_option_subtitles": "Subtitles/CC", - "search_filters_features_option_c_commons": "Creative Commons", - "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_features_option_location": "Location", - "search_filters_features_option_purchased": "Purchased", - "search_filters_sort_label": "Sort By", - "search_filters_sort_option_relevance": "Relevance", - "search_filters_sort_option_rating": "Rating", - "search_filters_sort_option_date": "Upload date", - "search_filters_sort_option_views": "View count", - "search_filters_apply_button": "Apply selected filters", - "Current version: ": "Current version: ", - "next_steps_error_message": "After which you should try to: ", - "next_steps_error_message_refresh": "Refresh", - "next_steps_error_message_go_to_youtube": "Go to YouTube", - "footer_donate_page": "Donate", - "footer_documentation": "Documentation", - "footer_source_code": "Source code", - "footer_original_source_code": "Original source code", - "footer_modfied_source_code": "Modified source code", - "adminprefs_modified_source_code_url_label": "URL to modified source code repository", - "none": "none", - "videoinfo_started_streaming_x_ago": "Started streaming `x` ago", - "videoinfo_watch_on_youTube": "Watch on YouTube", - "videoinfo_youTube_embed_link": "Embed", - "videoinfo_invidious_embed_link": "Embed Link", - "download_subtitles": "Subtitles - `x` (.vtt)", - "user_created_playlists": "`x` created playlists", - "user_saved_playlists": "`x` saved playlists", - "Video unavailable": "Video unavailable", - "preferences_save_player_pos_label": "Save playback position: ", - "crash_page_you_found_a_bug": "It looks like you found a bug in Invidious!", - "crash_page_before_reporting": "Before reporting a bug, make sure that you have:", - "crash_page_refresh": "tried to refresh the page", - "crash_page_switch_instance": "tried to use another instance", - "crash_page_read_the_faq": "read the Frequently Asked Questions (FAQ)", - "crash_page_search_issue": "searched for existing issues on GitHub", - "crash_page_report_issue": "If none of the above helped, please open a new issue on GitHub (preferably in English) and include the following text in your message (do NOT translate that text):", - "error_video_not_in_playlist": "The requested video doesn't exist in this playlist. Click here for the playlist home page.", - "channel_tab_videos_label": "Videos", - "channel_tab_shorts_label": "Shorts", - "channel_tab_streams_label": "Livestreams", - "channel_tab_podcasts_label": "Podcasts", - "channel_tab_releases_label": "Releases", - "channel_tab_courses_label": "Courses", - "channel_tab_playlists_label": "Playlists", - "channel_tab_community_label": "Community", - "channel_tab_posts_label": "Posts", - "channel_tab_channels_label": "Channels", - "toggle_theme": "Toggle Theme", - "carousel_slide": "Slide {{current}} of {{total}}", - "carousel_skip": "Skip the Carousel", - "carousel_go_to": "Go to slide `x`", - "preferences_trending_enabled_label": "Trending enabled: ", - "preferences_search_enabled_label": "Search enabled: ", - "timeline_parse_error_placeholder_heading": "Unable to parse item", - "timeline_parse_error_placeholder_message": "Invidious encountered an error while trying to parse this item. For more information see below:", - "timeline_parse_error_show_technical_details": "Show technical details", - "dmca_content": "This video cannot be downloaded on this instance due to a DMCA/copyright infringement letter sent to the instance administrator." + "Add to playlist": "Add to playlist", + "Add to playlist: ": "Add to playlist: ", + "Answer": "Answer", + "Search for videos": "Search for videos", + "popular_page_disabled": "The Popular feed has been disabled by the administrator.", + "trending_page_disabled": "The Trending feed has been disabled by the administrator.", + "search_page_disabled": "The Search feature has been disabled by the administrator.", + "generic_channels_count": "{{count}} channel", + "generic_channels_count_plural": "{{count}} channels", + "generic_views_count": "{{count}} view", + "generic_views_count_plural": "{{count}} views", + "generic_videos_count": "{{count}} video", + "generic_videos_count_plural": "{{count}} videos", + "generic_playlists_count": "{{count}} playlist", + "generic_playlists_count_plural": "{{count}} playlists", + "generic_subscribers_count": "{{count}} subscriber", + "generic_subscribers_count_plural": "{{count}} subscribers", + "generic_subscriptions_count": "{{count}} subscription", + "generic_subscriptions_count_plural": "{{count}} subscriptions", + "generic_button_delete": "Delete", + "generic_button_edit": "Edit", + "generic_button_save": "Save", + "generic_button_cancel": "Cancel", + "generic_button_rss": "RSS", + "LIVE": "LIVE", + "Shared `x` ago": "Shared `x` ago", + "Unsubscribe": "Unsubscribe", + "Subscribe": "Subscribe", + "View channel on YouTube": "View channel on YouTube", + "View playlist on YouTube": "View playlist on YouTube", + "newest": "newest", + "oldest": "oldest", + "popular": "popular", + "last": "last", + "Next page": "Next page", + "Previous page": "Previous page", + "First page": "First page", + "Clear watch history?": "Clear watch history?", + "New password": "New password", + "New passwords must match": "New passwords must match", + "Authorize token?": "Authorize token?", + "Authorize token for `x`?": "Authorize token for `x`?", + "Yes": "Yes", + "No": "No", + "Import and Export Data": "Import and Export Data", + "Import": "Import", + "Import Invidious data": "Import Invidious JSON data", + "Import YouTube subscriptions": "Import YouTube CSV or OPML subscriptions", + "Import YouTube playlist (.csv)": "Import YouTube playlist (.csv)", + "Import YouTube watch history (.json)": "Import YouTube watch history (.json)", + "Import FreeTube subscriptions (.db)": "Import FreeTube subscriptions (.db)", + "Import NewPipe subscriptions (.json)": "Import NewPipe subscriptions (.json)", + "Import NewPipe data (.zip)": "Import NewPipe data (.zip)", + "Export": "Export", + "Export subscriptions as OPML": "Export subscriptions as OPML", + "Export subscriptions as OPML (for NewPipe & FreeTube)": "Export subscriptions as OPML (for NewPipe & FreeTube)", + "Export data as JSON": "Export Invidious data as JSON", + "Delete account?": "Delete account?", + "History": "History", + "An alternative front-end to YouTube": "An alternative front-end to YouTube", + "JavaScript license information": "JavaScript license information", + "source": "source", + "Log in": "Log in", + "Log in/register": "Log in/register", + "User ID": "User ID", + "Password": "Password", + "Time (h:mm:ss):": "Time (h:mm:ss):", + "Sign In": "Sign In", + "Register": "Register", + "E-mail": "E-mail", + "Preferences": "Preferences", + "preferences_category_player": "Player preferences", + "preferences_video_loop_label": "Always loop: ", + "preferences_preload_label": "Preload video data: ", + "preferences_autoplay_label": "Autoplay: ", + "preferences_continue_label": "Play next by default: ", + "preferences_continue_autoplay_label": "Autoplay next video: ", + "preferences_listen_label": "Listen by default: ", + "preferences_local_label": "Proxy videos: ", + "preferences_watch_history_label": "Enable watch history: ", + "preferences_speed_label": "Default speed: ", + "preferences_quality_label": "Preferred video quality: ", + "preferences_quality_option_dash": "DASH (adaptive quality)", + "preferences_quality_option_hd720": "HD720", + "preferences_quality_option_medium": "Medium", + "preferences_quality_option_small": "Small", + "preferences_quality_dash_label": "Preferred DASH video quality: ", + "preferences_quality_dash_option_auto": "Auto", + "preferences_quality_dash_option_best": "Best", + "preferences_quality_dash_option_worst": "Worst", + "preferences_quality_dash_option_4320p": "4320p", + "preferences_quality_dash_option_2160p": "2160p", + "preferences_quality_dash_option_1440p": "1440p", + "preferences_quality_dash_option_1080p": "1080p", + "preferences_quality_dash_option_720p": "720p", + "preferences_quality_dash_option_480p": "480p", + "preferences_quality_dash_option_360p": "360p", + "preferences_quality_dash_option_240p": "240p", + "preferences_quality_dash_option_144p": "144p", + "preferences_volume_label": "Player volume: ", + "preferences_comments_label": "Default comments: ", + "youtube": "YouTube", + "reddit": "Reddit", + "invidious": "Invidious", + "preferences_captions_label": "Default captions: ", + "Fallback captions: ": "Fallback captions: ", + "preferences_related_videos_label": "Show related videos: ", + "preferences_annotations_label": "Show annotations by default: ", + "preferences_extend_desc_label": "Automatically extend video description: ", + "preferences_vr_mode_label": "Interactive 360 degree videos (requires WebGL): ", + "preferences_category_visual": "Visual preferences", + "preferences_region_label": "Content country: ", + "preferences_player_style_label": "Player style: ", + "Dark mode: ": "Dark mode: ", + "preferences_dark_mode_label": "Theme: ", + "dark": "dark", + "light": "light", + "preferences_thin_mode_label": "Thin mode: ", + "preferences_category_misc": "Miscellaneous preferences", + "preferences_automatic_instance_redirect_label": "Automatic instance redirection (fallback to redirect.invidious.io): ", + "preferences_category_subscription": "Subscription preferences", + "preferences_annotations_subscribed_label": "Show annotations by default for subscribed channels? ", + "Redirect homepage to feed: ": "Redirect homepage to feed: ", + "preferences_max_results_label": "Number of videos shown in feed: ", + "preferences_sort_label": "Sort videos by: ", + "preferences_default_playlist": "Default playlist: ", + "preferences_default_playlist_none": "No default playlist set", + "published": "published", + "published - reverse": "published - reverse", + "alphabetically": "alphabetically", + "alphabetically - reverse": "alphabetically - reverse", + "channel name": "channel name", + "channel name - reverse": "channel name - reverse", + "Only show latest video from channel: ": "Only show latest video from channel: ", + "Only show latest unwatched video from channel: ": "Only show latest unwatched video from channel: ", + "preferences_unseen_only_label": "Only show unwatched: ", + "preferences_notifications_only_label": "Only show notifications (if there are any): ", + "Enable web notifications": "Enable web notifications", + "`x` uploaded a video": "`x` uploaded a video", + "`x` is live": "`x` is live", + "preferences_category_data": "Data preferences", + "Clear watch history": "Clear watch history", + "Import/export data": "Import/export data", + "Change password": "Change password", + "Manage subscriptions": "Manage subscriptions", + "Manage tokens": "Manage tokens", + "Watch history": "Watch history", + "Delete account": "Delete account", + "preferences_category_admin": "Administrator preferences", + "preferences_default_home_label": "Default homepage: ", + "preferences_feed_menu_label": "Feed menu: ", + "preferences_show_nick_label": "Show nickname on top: ", + "Popular enabled: ": "Popular enabled: ", + "Top enabled: ": "Top enabled: ", + "CAPTCHA enabled: ": "CAPTCHA enabled: ", + "Login enabled: ": "Login enabled: ", + "Registration enabled: ": "Registration enabled: ", + "Report statistics: ": "Report statistics: ", + "Save preferences": "Save preferences", + "Subscription manager": "Subscription manager", + "Token manager": "Token manager", + "Token": "Token", + "tokens_count": "{{count}} token", + "tokens_count_plural": "{{count}} tokens", + "Import/export": "Import/export", + "unsubscribe": "unsubscribe", + "revoke": "revoke", + "Subscriptions": "Subscriptions", + "subscriptions_unseen_notifs_count": "{{count}} unseen notification", + "subscriptions_unseen_notifs_count_plural": "{{count}} unseen notifications", + "search": "search", + "Log out": "Log out", + "Released under the AGPLv3 on Github.": "Released under the AGPLv3 on GitHub.", + "Source available here.": "Source available here.", + "View JavaScript license information.": "View JavaScript license information.", + "View privacy policy.": "View privacy policy.", + "Trending": "Trending", + "Public": "Public", + "Unlisted": "Unlisted", + "Private": "Private", + "View all playlists": "View all playlists", + "Updated `x` ago": "Updated `x` ago", + "Delete playlist `x`?": "Delete playlist `x`?", + "Delete playlist": "Delete playlist", + "Create playlist": "Create playlist", + "Title": "Title", + "Playlist privacy": "Playlist privacy", + "Editing playlist `x`": "Editing playlist `x`", + "playlist_button_add_items": "Add videos", + "Show more": "Show more", + "Show less": "Show less", + "Watch on YouTube": "Watch on YouTube", + "Switch Invidious Instance": "Switch Invidious Instance", + "search_message_no_results": "No results found.", + "search_message_change_filters_or_query": "Try widening your search query and/or changing the filters.", + "search_message_use_another_instance": "You can also search on another instance.", + "Hide annotations": "Hide annotations", + "Show annotations": "Show annotations", + "Genre: ": "Genre: ", + "License: ": "License: ", + "Standard YouTube license": "Standard YouTube license", + "Family friendly? ": "Family friendly? ", + "Wilson score: ": "Wilson score: ", + "Engagement: ": "Engagement: ", + "Whitelisted regions: ": "Whitelisted regions: ", + "Blacklisted regions: ": "Blacklisted regions: ", + "Music in this video": "Music in this video", + "Artist: ": "Artist: ", + "Song: ": "Song: ", + "Album: ": "Album: ", + "Shared `x`": "Shared `x`", + "Premieres in `x`": "Premieres in `x`", + "Premieres `x`": "Premieres `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.": "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.", + "View YouTube comments": "View YouTube comments", + "View more comments on Reddit": "View more comments on Reddit", + "View `x` comments": { + "([^.,0-9]|^)1([^.,0-9]|$)": "View `x` comment", + "": "View `x` comments" + }, + "View Reddit comments": "View Reddit comments", + "Hide replies": "Hide replies", + "Show replies": "Show replies", + "Incorrect password": "Incorrect password", + "Wrong answer": "Wrong answer", + "Erroneous CAPTCHA": "Erroneous CAPTCHA", + "CAPTCHA is a required field": "CAPTCHA is a required field", + "User ID is a required field": "User ID is a required field", + "Password is a required field": "Password is a required field", + "Wrong username or password": "Wrong username or password", + "Password cannot be empty": "Password cannot be empty", + "Password cannot be longer than 55 characters": "Password cannot be longer than 55 characters", + "Please log in": "Please log in", + "Invidious Private Feed for `x`": "Invidious Private Feed for `x`", + "channel:`x`": "channel:`x`", + "Deleted or invalid channel": "Deleted or invalid channel", + "This channel does not exist.": "This channel does not exist.", + "Could not get channel info.": "Could not get channel info.", + "Could not fetch comments": "Could not fetch comments", + "comments_view_x_replies": "View {{count}} reply", + "comments_view_x_replies_plural": "View {{count}} replies", + "`x` ago": "`x` ago", + "Load more": "Load more", + "comments_points_count": "{{count}} point", + "comments_points_count_plural": "{{count}} points", + "Could not create mix.": "Could not create mix.", + "Empty playlist": "Empty playlist", + "Not a playlist.": "Not a playlist.", + "Playlist does not exist.": "Playlist does not exist.", + "Could not pull trending pages.": "Could not pull trending pages.", + "Hidden field \"challenge\" is a required field": "Hidden field \"challenge\" is a required field", + "Hidden field \"token\" is a required field": "Hidden field \"token\" is a required field", + "Erroneous challenge": "Erroneous challenge", + "Erroneous token": "Erroneous token", + "No such user": "No such user", + "Token is expired, please try again": "Token is expired, please try again", + "English": "English", + "English (United Kingdom)": "English (United Kingdom)", + "English (United States)": "English (United States)", + "English (auto-generated)": "English (auto-generated)", + "Afrikaans": "Afrikaans", + "Albanian": "Albanian", + "Amharic": "Amharic", + "Arabic": "Arabic", + "Armenian": "Armenian", + "Azerbaijani": "Azerbaijani", + "Bangla": "Bangla", + "Basque": "Basque", + "Belarusian": "Belarusian", + "Bosnian": "Bosnian", + "Bulgarian": "Bulgarian", + "Burmese": "Burmese", + "Cantonese (Hong Kong)": "Cantonese (Hong Kong)", + "Catalan": "Catalan", + "Cebuano": "Cebuano", + "Chinese": "Chinese", + "Chinese (China)": "Chinese (China)", + "Chinese (Hong Kong)": "Chinese (Hong Kong)", + "Chinese (Simplified)": "Chinese (Simplified)", + "Chinese (Taiwan)": "Chinese (Taiwan)", + "Chinese (Traditional)": "Chinese (Traditional)", + "Corsican": "Corsican", + "Croatian": "Croatian", + "Czech": "Czech", + "Danish": "Danish", + "Dutch": "Dutch", + "Dutch (auto-generated)": "Dutch (auto-generated)", + "Esperanto": "Esperanto", + "Estonian": "Estonian", + "Filipino": "Filipino", + "Filipino (auto-generated)": "Filipino (auto-generated)", + "Finnish": "Finnish", + "French": "French", + "French (auto-generated)": "French (auto-generated)", + "Galician": "Galician", + "Georgian": "Georgian", + "German": "German", + "German (auto-generated)": "German (auto-generated)", + "Greek": "Greek", + "Gujarati": "Gujarati", + "Haitian Creole": "Haitian Creole", + "Hausa": "Hausa", + "Hawaiian": "Hawaiian", + "Hebrew": "Hebrew", + "Hindi": "Hindi", + "Hmong": "Hmong", + "Hungarian": "Hungarian", + "Icelandic": "Icelandic", + "Igbo": "Igbo", + "Indonesian": "Indonesian", + "Indonesian (auto-generated)": "Indonesian (auto-generated)", + "Interlingue": "Interlingue", + "Irish": "Irish", + "Italian": "Italian", + "Italian (auto-generated)": "Italian (auto-generated)", + "Japanese": "Japanese", + "Japanese (auto-generated)": "Japanese (auto-generated)", + "Javanese": "Javanese", + "Kannada": "Kannada", + "Kazakh": "Kazakh", + "Khmer": "Khmer", + "Korean": "Korean", + "Korean (auto-generated)": "Korean (auto-generated)", + "Kurdish": "Kurdish", + "Kyrgyz": "Kyrgyz", + "Lao": "Lao", + "Latin": "Latin", + "Latvian": "Latvian", + "Lithuanian": "Lithuanian", + "Luxembourgish": "Luxembourgish", + "Macedonian": "Macedonian", + "Malagasy": "Malagasy", + "Malay": "Malay", + "Malayalam": "Malayalam", + "Maltese": "Maltese", + "Maori": "Maori", + "Marathi": "Marathi", + "Mongolian": "Mongolian", + "Nepali": "Nepali", + "Norwegian Bokmål": "Norwegian Bokmål", + "Nyanja": "Nyanja", + "Pashto": "Pashto", + "Persian": "Persian", + "Polish": "Polish", + "Portuguese": "Portuguese", + "Portuguese (auto-generated)": "Portuguese (auto-generated)", + "Portuguese (Brazil)": "Portuguese (Brazil)", + "Punjabi": "Punjabi", + "Romanian": "Romanian", + "Russian": "Russian", + "Russian (auto-generated)": "Russian (auto-generated)", + "Samoan": "Samoan", + "Scottish Gaelic": "Scottish Gaelic", + "Serbian": "Serbian", + "Shona": "Shona", + "Sindhi": "Sindhi", + "Sinhala": "Sinhala", + "Slovak": "Slovak", + "Slovenian": "Slovenian", + "Somali": "Somali", + "Southern Sotho": "Southern Sotho", + "Spanish": "Spanish", + "Spanish (auto-generated)": "Spanish (auto-generated)", + "Spanish (Latin America)": "Spanish (Latin America)", + "Spanish (Mexico)": "Spanish (Mexico)", + "Spanish (Spain)": "Spanish (Spain)", + "Sundanese": "Sundanese", + "Swahili": "Swahili", + "Swedish": "Swedish", + "Tajik": "Tajik", + "Tamil": "Tamil", + "Telugu": "Telugu", + "Thai": "Thai", + "Turkish": "Turkish", + "Turkish (auto-generated)": "Turkish (auto-generated)", + "Ukrainian": "Ukrainian", + "Urdu": "Urdu", + "Uzbek": "Uzbek", + "Vietnamese": "Vietnamese", + "Vietnamese (auto-generated)": "Vietnamese (auto-generated)", + "Welsh": "Welsh", + "Western Frisian": "Western Frisian", + "Xhosa": "Xhosa", + "Yiddish": "Yiddish", + "Yoruba": "Yoruba", + "Zulu": "Zulu", + "generic_count_years": "{{count}} year", + "generic_count_years_plural": "{{count}} years", + "generic_count_months": "{{count}} month", + "generic_count_months_plural": "{{count}} months", + "generic_count_weeks": "{{count}} week", + "generic_count_weeks_plural": "{{count}} weeks", + "generic_count_days": "{{count}} day", + "generic_count_days_plural": "{{count}} days", + "generic_count_hours": "{{count}} hour", + "generic_count_hours_plural": "{{count}} hours", + "generic_count_minutes": "{{count}} minute", + "generic_count_minutes_plural": "{{count}} minutes", + "generic_count_seconds": "{{count}} second", + "generic_count_seconds_plural": "{{count}} seconds", + "Fallback comments: ": "Fallback comments: ", + "Popular": "Popular", + "Search": "Search", + "Top": "Top", + "About": "About", + "Rating: ": "Rating: ", + "preferences_locale_label": "Language: ", + "View as playlist": "View as playlist", + "Default": "Default", + "Music": "Music", + "Gaming": "Gaming", + "Livestreams": "Livestreams", + "News": "News", + "Movies": "Movies", + "Download": "Download", + "Download as: ": "Download as: ", + "Download is disabled": "Download is disabled", + "%A %B %-d, %Y": "%A %B %-d, %Y", + "(edited)": "(edited)", + "YouTube comment permalink": "YouTube comment permalink", + "permalink": "permalink", + "`x` marked it with a ❤": "`x` marked it with a ❤", + "Channel Sponsor": "Channel Sponsor", + "Audio mode": "Audio mode", + "Video mode": "Video mode", + "Playlists": "Playlists", + "search_filters_title": "Filters", + "search_filters_date_label": "Upload date", + "search_filters_date_option_none": "Any date", + "search_filters_date_option_hour": "Last hour", + "search_filters_date_option_today": "Today", + "search_filters_date_option_week": "This week", + "search_filters_date_option_month": "This month", + "search_filters_date_option_year": "This year", + "search_filters_type_label": "Type", + "search_filters_type_option_all": "Any type", + "search_filters_type_option_video": "Video", + "search_filters_type_option_channel": "Channel", + "search_filters_type_option_playlist": "Playlist", + "search_filters_type_option_movie": "Movie", + "search_filters_type_option_show": "Show", + "search_filters_duration_label": "Duration", + "search_filters_duration_option_none": "Any duration", + "search_filters_duration_option_short": "Short (< 4 minutes)", + "search_filters_duration_option_medium": "Medium (4 - 20 minutes)", + "search_filters_duration_option_long": "Long (> 20 minutes)", + "search_filters_features_label": "Features", + "search_filters_features_option_live": "Live", + "search_filters_features_option_four_k": "4K", + "search_filters_features_option_hd": "HD", + "search_filters_features_option_subtitles": "Subtitles/CC", + "search_filters_features_option_c_commons": "Creative Commons", + "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_features_option_location": "Location", + "search_filters_features_option_purchased": "Purchased", + "search_filters_sort_label": "Sort By", + "search_filters_sort_option_relevance": "Relevance", + "search_filters_sort_option_rating": "Rating", + "search_filters_sort_option_date": "Upload date", + "search_filters_sort_option_views": "View count", + "search_filters_apply_button": "Apply selected filters", + "Current version: ": "Current version: ", + "next_steps_error_message": "After which you should try to: ", + "next_steps_error_message_refresh": "Refresh", + "next_steps_error_message_go_to_youtube": "Go to YouTube", + "footer_donate_page": "Donate", + "footer_documentation": "Documentation", + "footer_source_code": "Source code", + "footer_original_source_code": "Original source code", + "footer_modfied_source_code": "Modified source code", + "adminprefs_modified_source_code_url_label": "URL to modified source code repository", + "none": "none", + "videoinfo_started_streaming_x_ago": "Started streaming `x` ago", + "videoinfo_watch_on_youTube": "Watch on YouTube", + "videoinfo_youTube_embed_link": "Embed", + "videoinfo_invidious_embed_link": "Embed Link", + "download_subtitles": "Subtitles - `x` (.vtt)", + "user_created_playlists": "`x` created playlists", + "user_saved_playlists": "`x` saved playlists", + "Video unavailable": "Video unavailable", + "preferences_save_player_pos_label": "Save playback position: ", + "crash_page_you_found_a_bug": "It looks like you found a bug in Invidious!", + "crash_page_before_reporting": "Before reporting a bug, make sure that you have:", + "crash_page_refresh": "tried to refresh the page", + "crash_page_switch_instance": "tried to use another instance", + "crash_page_read_the_faq": "read the Frequently Asked Questions (FAQ)", + "crash_page_search_issue": "searched for existing issues on GitHub", + "crash_page_report_issue": "If none of the above helped, please open a new issue on GitHub (preferably in English) and include the following text in your message (do NOT translate that text):", + "error_video_not_in_playlist": "The requested video doesn't exist in this playlist. Click here for the playlist home page.", + "channel_tab_videos_label": "Videos", + "channel_tab_shorts_label": "Shorts", + "channel_tab_streams_label": "Livestreams", + "channel_tab_podcasts_label": "Podcasts", + "channel_tab_releases_label": "Releases", + "channel_tab_courses_label": "Courses", + "channel_tab_playlists_label": "Playlists", + "channel_tab_community_label": "Community", + "channel_tab_posts_label": "Posts", + "channel_tab_channels_label": "Channels", + "toggle_theme": "Toggle Theme", + "carousel_slide": "Slide {{current}} of {{total}}", + "carousel_skip": "Skip the Carousel", + "carousel_go_to": "Go to slide `x`", + "preferences_trending_enabled_label": "Trending enabled: ", + "preferences_search_enabled_label": "Search enabled: ", + "timeline_parse_error_placeholder_heading": "Unable to parse item", + "timeline_parse_error_placeholder_message": "Invidious encountered an error while trying to parse this item. For more information see below:", + "timeline_parse_error_show_technical_details": "Show technical details", + "dmca_content": "This video cannot be downloaded on this instance due to a DMCA/copyright infringement letter sent to the instance administrator." } diff --git a/src/ext/kemal_static_file_handler.cr b/src/ext/kemal_static_file_handler.cr index 16cb84fb..9266d51e 100644 --- a/src/ext/kemal_static_file_handler.cr +++ b/src/ext/kemal_static_file_handler.cr @@ -185,7 +185,7 @@ module Kemal if is_dir if config.is_a?(Hash) && config["dir_listing"] == true context.response.content_type = "text/html" - directory_listing(context.response, request_path, file_path) + directory_listing(context.response, Path[request_path], Path[file_path]) else call_next(context) end diff --git a/src/invidious/routes/debug_config.cr b/src/invidious/routes/debug_config.cr new file mode 100644 index 00000000..8378929b --- /dev/null +++ b/src/invidious/routes/debug_config.cr @@ -0,0 +1,114 @@ +# Debug route to verify configuration is loaded correctly +# This can help diagnose issues with pages_enabled configuration + +module Invidious::Routes::DebugConfig + def self.show(env) + # Only allow access to admins or in development mode + if CONFIG.admins.empty? || (user = env.get? "user") + admin_user = user.try &.as(User) + if !admin_user || !CONFIG.admins.includes?(admin_user.email) + return error_template(403, "Administrator privileges required") + end + else + # If no user is logged in and admins are configured, deny access + return error_template(403, "Administrator privileges required") + end + + html = <<-HTML + + + + Configuration Debug - Invidious + + + +

Invidious Configuration Debug

+ +

Pages Configuration

+ + + + + + + + + + + + + + + + + + + + + +
PageStatusConfiguration Value
Popular + #{CONFIG.page_enabled?("popular") ? "ENABLED" : "DISABLED"} + #{CONFIG.pages_enabled.popular}
Trending + #{CONFIG.page_enabled?("trending") ? "ENABLED" : "DISABLED"} + #{CONFIG.pages_enabled.trending}
Search + #{CONFIG.page_enabled?("search") ? "ENABLED" : "DISABLED"} + #{CONFIG.pages_enabled.search}
+ +

Configuration Flags

+ + + + + + + + + + + + + + + + + +
FlagValue
pages_enabled_present#{CONFIG.pages_enabled_present}
popular_enabled_present (deprecated)#{CONFIG.popular_enabled_present}
popular_enabled (deprecated)#{CONFIG.popular_enabled}
+ +

Blocked Routes

+

The following routes should be blocked based on current configuration:

+
    + #{!CONFIG.page_enabled?("popular") ? "
  • /feed/popular
  • /api/v1/popular
  • " : ""} + #{!CONFIG.page_enabled?("trending") ? "
  • /feed/trending
  • /api/v1/trending
  • " : ""} + #{!CONFIG.page_enabled?("search") ? "
  • /search
  • /api/v1/search
  • " : ""} +
+ +

Test Links

+

Click these links to verify they are properly blocked:

+
    +
  • /feed/popular - #{CONFIG.page_enabled?("popular") ? "Should work" : "Should be blocked"}
  • +
  • /feed/trending - #{CONFIG.page_enabled?("trending") ? "Should work" : "Should be blocked"}
  • +
  • /search - #{CONFIG.page_enabled?("search") ? "Should work" : "Should be blocked"}
  • +
+ +

Raw Configuration

+
pages_enabled: #{CONFIG.pages_enabled.inspect}
+ +

Environment Check

+
INVIDIOUS_CONFIG present: #{ENV.has_key?("INVIDIOUS_CONFIG")}
+
INVIDIOUS_PAGES_ENABLED present: #{ENV.has_key?("INVIDIOUS_PAGES_ENABLED")}
+ + + HTML + + env.response.content_type = "text/html" + env.response.print html + end +end \ No newline at end of file diff --git a/src/invidious/views/user/preferences.ecr b/src/invidious/views/user/preferences.ecr index 0af2be0a..80011e65 100644 --- a/src/invidious/views/user/preferences.ecr +++ b/src/invidious/views/user/preferences.ecr @@ -182,11 +182,20 @@ checked<% end %>>
- <% if env.get?("user") %> - <% feed_options = {"", "Popular", "Trending", "Subscriptions", "Playlists"} %> - <% else %> - <% feed_options = {"", "Popular", "Trending"} %> - <% end %> + <% + # Build feed options based on enabled pages + feed_options = [] of String + # Empty string represents Search page + feed_options << "" if CONFIG.page_enabled?("search") + feed_options << "Popular" if CONFIG.page_enabled?("popular") + feed_options << "Trending" if CONFIG.page_enabled?("trending") + if env.get?("user") + feed_options << "Subscriptions" + feed_options << "Playlists" + end + # Always add "none" option as fallback + feed_options << "" if !feed_options.includes?("") + %>
From ea36b2ba4b2d9efe52396adf9f766e1a29e1c20d Mon Sep 17 00:00:00 2001 From: NorkzYT Date: Sun, 22 Mar 2026 21:10:55 +0000 Subject: [PATCH 28/33] fix(preferences): unchecked pages_enabled checkboxes now persist correctly HTML checkboxes send no value when unchecked, so the fallback "|| \"on\"" always re-enabled popular/trending/search on save. Changed default to "off" to match the pattern used by captcha_enabled, login_enabled, and other checkbox toggles in the same handler. --- src/invidious/routes/preferences.cr | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/invidious/routes/preferences.cr b/src/invidious/routes/preferences.cr index 1c4f4c9d..154f4637 100644 --- a/src/invidious/routes/preferences.cr +++ b/src/invidious/routes/preferences.cr @@ -202,9 +202,9 @@ module Invidious::Routes::PreferencesRoute CONFIG.default_user_preferences.feed_menu = admin_feed_menu CONFIG.pages_enabled = PagesEnabled.new( - popular: (env.params.body["popular_enabled"]?.try &.as(String) || "on") == "on", - trending: (env.params.body["trending_enabled"]?.try &.as(String) || "on") == "on", - search: (env.params.body["search_enabled"]?.try &.as(String) || "on") == "on", + popular: (env.params.body["popular_enabled"]?.try &.as(String) || "off") == "on", + trending: (env.params.body["trending_enabled"]?.try &.as(String) || "off") == "on", + search: (env.params.body["search_enabled"]?.try &.as(String) || "off") == "on", ) captcha_enabled = env.params.body["captcha_enabled"]?.try &.as(String) From 562389bc6d91e98c15875921d30c07805171bce8 Mon Sep 17 00:00:00 2001 From: NorkzYT Date: Sun, 22 Mar 2026 21:42:11 +0000 Subject: [PATCH 29/33] fix(pages_enabled): hide search UI and block all search routes when disabled - Hide navbar search box when CONFIG.page_enabled?("search") is false - Keep Invidious logo visible with proper width when search bar is hidden - Hide search widget on search_homepage when search is disabled - Block /results, /api/v1/search/suggestions in before_all guard - Block /hashtag/* and /api/v1/hashtag/* when search is disabled --- src/invidious/routes/before_all.cr | 4 +++- src/invidious/views/search_homepage.ecr | 2 ++ src/invidious/views/template.ecr | 6 +++++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/invidious/routes/before_all.cr b/src/invidious/routes/before_all.cr index 90958934..841bbab2 100644 --- a/src/invidious/routes/before_all.cr +++ b/src/invidious/routes/before_all.cr @@ -150,7 +150,9 @@ module Invidious::Routes::BeforeAll "popular" when "/feed/trending", "/api/v1/trending" "trending" - when "/search", "/api/v1/search" + when "/search", "/api/v1/search", "/api/v1/search/suggestions", "/results" + "search" + when .starts_with?("/hashtag/"), .starts_with?("/api/v1/hashtag/") "search" else nil diff --git a/src/invidious/views/search_homepage.ecr b/src/invidious/views/search_homepage.ecr index 2424a1cf..f651dc5c 100644 --- a/src/invidious/views/search_homepage.ecr +++ b/src/invidious/views/search_homepage.ecr @@ -12,9 +12,11 @@ + <% if CONFIG.page_enabled?("search") %>
+ <% end %>
diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr index 40f5544f..9aa45e68 100644 --- a/src/invidious/views/template.ecr +++ b/src/invidious/views/template.ecr @@ -31,13 +31,17 @@