kopia lustrzana https://github.com/iv-org/invidious
				
				
				
			Merge 20e4e52b8b into 5cfe294063
				
					
				
			
						commit
						ef9772addd
					
				|  | @ -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 | ||||
| ## Default: true | ||||
| #pages_enabled: | ||||
| #  trending: true | ||||
| #  popular: true | ||||
| #  search: true | ||||
| ## | ||||
| #popular_enabled: true | ||||
| 
 | ||||
| ## | ||||
| ## Enable/Disable statstics (available at /api/v1/stats). | ||||
|  |  | |||
|  | @ -3,7 +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.", | ||||
|     "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", | ||||
|  | @ -503,7 +504,10 @@ | |||
|     "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" | ||||
| } | ||||
|     "timeline_parse_error_show_technical_details": "Show technical details", | ||||
|     "search_page_disabled": "Search has been disabled by the administrator." | ||||
| } | ||||
|  | @ -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 | ||||
|  | @ -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) | ||||
| end | ||||
| 
 | ||||
| if CONFIG.popular_enabled | ||||
| if CONFIG.page_enabled?("popular") | ||||
|   Invidious::Jobs.register Invidious::Jobs::PullPopularVideosJob.new(PG_DB) | ||||
| end | ||||
| 
 | ||||
|  |  | |||
|  | @ -73,6 +73,31 @@ struct HTTPProxyConfig | |||
|   property port : Int32 | ||||
| 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 | ||||
|   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:// | ||||
|   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 | ||||
| 
 | ||||
|   # ————————————————————————————————————————————————————————————————————————————————————— | ||||
| 
 | ||||
|   # 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 | ||||
|   property login_enabled : Bool = true | ||||
|   property registration_enabled : Bool = true | ||||
|  | @ -193,16 +242,17 @@ 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 | ||||
|     return @pages_enabled[page] | ||||
|   end | ||||
| 
 | ||||
|   def self.load | ||||
|     # Load config from file or YAML string env var | ||||
|     env_config_file = "INVIDIOUS_CONFIG_FILE" | ||||
|  | @ -240,6 +290,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 | ||||
|  | @ -297,6 +353,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 | ||||
|  | @ -334,4 +392,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 | ||||
|  |  | |||
|  | @ -29,11 +29,6 @@ module Invidious::Routes::API::V1::Feeds | |||
| 
 | ||||
|     env.response.content_type = "application/json" | ||||
| 
 | ||||
|     if !CONFIG.popular_enabled | ||||
|       error_message = {"error" => "Administrator has disabled this endpoint."}.to_json | ||||
|       haltf env, 403, error_message | ||||
|     end | ||||
| 
 | ||||
|     JSON.build do |json| | ||||
|       json.array do | ||||
|         popular_videos.each do |video| | ||||
|  |  | |||
|  | @ -103,6 +103,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 | ||||
|     # | ||||
|  |  | |||
|  | @ -33,18 +33,11 @@ module Invidious::Routes::Feeds | |||
| 
 | ||||
|   def self.popular(env) | ||||
|     locale = env.get("preferences").as(Preferences).locale | ||||
| 
 | ||||
|     if CONFIG.popular_enabled | ||||
|       templated "feeds/popular" | ||||
|     else | ||||
|       message = translate(locale, "The Popular feed has been disabled by the administrator.") | ||||
|       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" | ||||
| 
 | ||||
|  |  | |||
|  | @ -202,9 +202,11 @@ 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" | ||||
|         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", | ||||
|         ) | ||||
| 
 | ||||
|         captcha_enabled = env.params.body["captcha_enabled"]?.try &.as(String) | ||||
|         captcha_enabled ||= "off" | ||||
|  |  | |||
|  | @ -40,52 +40,47 @@ module Invidious::Routes::Search | |||
|     prefs = env.get("preferences").as(Preferences) | ||||
|     locale = prefs.locale | ||||
| 
 | ||||
|     # 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? | ||||
|       # 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 | ||||
|         env.set "search", query.text | ||||
|       end | ||||
| 
 | ||||
|       templated "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 | ||||
| 
 | ||||
|     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 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) | ||||
|  |  | |||
|  | @ -302,9 +302,18 @@ | |||
| 
 | ||||
|                 <div class="pure-control-group"> | ||||
|                     <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 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"> | ||||
|                     <label for="captcha_enabled"><%= translate(locale, "CAPTCHA enabled: ") %></label> | ||||
|  |  | |||
		Ładowanie…
	
		Reference in New Issue
	
	 Richard Lora
						Richard Lora