This commit is contained in:
Richard Lora 2025-09-14 07:31:32 +00:00 committed by GitHub
commit b08cc5b117
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 220 additions and 71 deletions

View File

@ -312,12 +312,13 @@ https_only: false
# ----------------------------- # -----------------------------
## ##
## Enable/Disable the "Popular" tab on the main page. ## Enable/Disable specific pages on the main page.
## ##
## Accepted values: true, false #pages_enabled:
## Default: true # trending: true
# popular: true
# search: true
## ##
#popular_enabled: true
## ##
## Enable/Disable statstics (available at /api/v1/stats). ## Enable/Disable statstics (available at /api/v1/stats).

View File

@ -3,7 +3,8 @@
"Add to playlist: ": "Add to playlist: ", "Add to playlist: ": "Add to playlist: ",
"Answer": "Answer", "Answer": "Answer",
"Search for videos": "Search for videos", "Search for videos": "Search for videos",
"The Popular feed has been disabled by the administrator.": "The Popular 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": "{{count}} channel",
"generic_channels_count_plural": "{{count}} channels", "generic_channels_count_plural": "{{count}} channels",
"generic_views_count": "{{count}} view", "generic_views_count": "{{count}} view",
@ -502,7 +503,10 @@
"carousel_slide": "Slide {{current}} of {{total}}", "carousel_slide": "Slide {{current}} of {{total}}",
"carousel_skip": "Skip the Carousel", "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: ",
"timeline_parse_error_placeholder_heading": "Unable to parse item", "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_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_page_disabled": "Search has been disabled by the administrator."
} }

View File

@ -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

View File

@ -197,7 +197,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) Invidious::Jobs.register Invidious::Jobs::SubscribeToFeedsJob.new(PG_DB, HMAC_KEY)
end end
if CONFIG.popular_enabled if CONFIG.page_enabled?("popular")
Invidious::Jobs.register Invidious::Jobs::PullPopularVideosJob.new(PG_DB) Invidious::Jobs.register Invidious::Jobs::PullPopularVideosJob.new(PG_DB)
end end

View File

@ -73,6 +73,31 @@ struct HTTPProxyConfig
property port : Int32 property port : Int32
end end
# Structure used for global per-page feature toggles
record PagesEnabled,
trending : Bool = true,
popular : Bool = true,
search : Bool = true do
include YAML::Serializable
def [](key : String) : Bool
fetch(key) { raise KeyError.new("Unknown page '#{key}'") }
end
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 yield
end
end
end
class Config class Config
include YAML::Serializable include YAML::Serializable
@ -116,13 +141,37 @@ class Config
# Used to tell Invidious it is behind a proxy, so links to resources should be https:// # Used to tell Invidious it is behind a proxy, so links to resources should be https://
property https_only : Bool? property https_only : Bool?
# HMAC signing key for CSRF tokens and verifying pubsub subscriptions # HMAC signing key for CSRF tokens and verifying pubsub subscriptions
property hmac_key : String = "" property hmac_key : String = ""
# Domain to be used for links to resources on the site where an absolute URL is required # Domain to be used for links to resources on the site where an absolute URL is required
property domain : String? property domain : String?
# Subscribe to channels using PubSubHubbub (requires domain, hmac_key) # Subscribe to channels using PubSubHubbub (requires domain, hmac_key)
property use_pubsub_feeds : Bool | Int32 = false 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 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 property captcha_enabled : Bool = true
property login_enabled : Bool = true property login_enabled : Bool = true
property registration_enabled : Bool = true property registration_enabled : Bool = true
@ -193,14 +242,15 @@ class Config
when Bool when Bool
return disabled return disabled
when Array when Array
if disabled.includes? option disabled.includes?(option)
return true
else else
return false false
end end
else
return false
end end
# Centralized page toggle with legacy fallback for `popular_enabled`
def page_enabled?(page : String) : Bool
return @pages_enabled[page]
end end
def self.load def self.load
@ -240,6 +290,12 @@ class Config
begin begin
config.{{ivar.id}} = ivar_type.from_yaml(env_value) config.{{ivar.id}} = ivar_type.from_yaml(env_value)
success = true 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 rescue
# nop # nop
end end
@ -297,6 +353,8 @@ class Config
exit(1) exit(1)
end end
config.process_deprecation
# Build database_url from db.* if it's not set directly # Build database_url from db.* if it's not set directly
if config.database_url.to_s.empty? if config.database_url.to_s.empty?
if db = config.db if db = config.db
@ -334,4 +392,24 @@ class Config
return config return config
end 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 end

View File

@ -29,11 +29,6 @@ module Invidious::Routes::API::V1::Feeds
env.response.content_type = "application/json" env.response.content_type = "application/json"
if !CONFIG.popular_enabled
error_message = {"error" => "Administrator has disabled this endpoint."}.to_json
haltf env, 403, error_message
end
JSON.build do |json| JSON.build do |json|
json.array do json.array do
popular_videos.each do |video| popular_videos.each do |video|

View File

@ -103,6 +103,28 @@ module Invidious::Routes::BeforeAll
preferences.locale = locale preferences.locale = locale
env.set "preferences", preferences 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 # Allow media resources to be loaded from google servers
# TODO: check if *.youtube.com can be removed # TODO: check if *.youtube.com can be removed
# #

View File

@ -33,18 +33,11 @@ module Invidious::Routes::Feeds
def self.popular(env) def self.popular(env)
locale = env.get("preferences").as(Preferences).locale locale = env.get("preferences").as(Preferences).locale
if CONFIG.popular_enabled
templated "feeds/popular" templated "feeds/popular"
else
message = translate(locale, "The Popular feed has been disabled by the administrator.")
templated "message"
end
end end
def self.trending(env) def self.trending(env)
locale = env.get("preferences").as(Preferences).locale locale = env.get("preferences").as(Preferences).locale
trending_type = env.params.query["type"]? trending_type = env.params.query["type"]?
trending_type ||= "Default" trending_type ||= "Default"

View File

@ -202,9 +202,11 @@ module Invidious::Routes::PreferencesRoute
end end
CONFIG.default_user_preferences.feed_menu = admin_feed_menu CONFIG.default_user_preferences.feed_menu = admin_feed_menu
popular_enabled = env.params.body["popular_enabled"]?.try &.as(String) CONFIG.pages_enabled = PagesEnabled.new(
popular_enabled ||= "off" popular: (env.params.body["popular_enabled"]?.try &.as(String) || "on") == "on",
CONFIG.popular_enabled = popular_enabled == "on" trending: (env.params.body["trending_enabled"]?.try &.as(String) || "on") == "on",
search: (env.params.body["search_enabled"]?.try &.as(String) || "on") == "on",
)
captcha_enabled = env.params.body["captcha_enabled"]?.try &.as(String) captcha_enabled = env.params.body["captcha_enabled"]?.try &.as(String)
captcha_enabled ||= "off" captcha_enabled ||= "off"

View File

@ -40,33 +40,28 @@ module Invidious::Routes::Search
prefs = env.get("preferences").as(Preferences) prefs = env.get("preferences").as(Preferences)
locale = prefs.locale locale = prefs.locale
# otherwise, do a normal search
region = env.params.query["region"]? || prefs.region 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)
# empty query → show homepage
if query.empty? if query.empty?
# Display the full page search box implemented in #1977
env.set "search", "" env.set "search", ""
templated "search_homepage", navbar_search: false return templated "search_homepage", navbar_search: false
else end
user = env.get? "user"
# An URL was copy/pasted in the search box. # nonempty query → process it
# Redirect the user to the appropriate page. user = env.get?("user")
if query.url? if query.url?
return env.redirect UrlSanitizer.process(query.text).to_s return env.redirect UrlSanitizer.process(query.text).to_s
end end
begin begin
if user items = user ? query.process(user.as(User)) : query.process
items = query.process(user.as(User))
else
items = query.process
end
rescue ex : ChannelSearchException 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'.") return error_template 404, "Unable to find channel with id “#{HTML.escape(ex.channel)}”…"
rescue ex rescue ex
return error_template(500, ex) return error_template 500, ex
end end
redirect_url = Invidious::Frontend::Misc.redirect_url(env) redirect_url = Invidious::Frontend::Misc.redirect_url(env)
@ -78,6 +73,7 @@ module Invidious::Routes::Search
show_next: (items.size >= 20) 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 if query.type == Invidious::Search::Query::Type::Channel
env.set "search", "channel:#{query.channel} #{query.text}" env.set "search", "channel:#{query.channel} #{query.text}"
else else
@ -86,7 +82,6 @@ module Invidious::Routes::Search
templated "search" templated "search"
end end
end
def self.hashtag(env : HTTP::Server::Context) def self.hashtag(env : HTTP::Server::Context)
locale = env.get("preferences").as(Preferences).locale locale = env.get("preferences").as(Preferences).locale

View File

@ -302,9 +302,18 @@
<div class="pure-control-group"> <div class="pure-control-group">
<label for="popular_enabled"><%= translate(locale, "Popular enabled: ") %></label> <label for="popular_enabled"><%= translate(locale, "Popular enabled: ") %></label>
<input name="popular_enabled" id="popular_enabled" type="checkbox" <% if CONFIG.popular_enabled %>checked<% end %>> <input name="popular_enabled" id="popular_enabled" type="checkbox" <% if CONFIG.page_enabled?("popular") %>checked<% end %>>
</div> </div>
<div class="pure-control-group">
<label for="trending_enabled"><%= translate(locale, "preferences_trending_enabled_label") %></label>
<input name="trending_enabled" id="trending_enabled" type="checkbox" <% if CONFIG.page_enabled?("trending") %>checked<% end %>>
</div>
<div class="pure-control-group">
<label for="search_enabled"><%= translate(locale, "preferences_search_enabled_label") %></label>
<input name="search_enabled" id="search_enabled" type="checkbox" <% if CONFIG.page_enabled?("search") %>checked<% end %>>
</div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="captcha_enabled"><%= translate(locale, "CAPTCHA enabled: ") %></label> <label for="captcha_enabled"><%= translate(locale, "CAPTCHA enabled: ") %></label>