struct DBConfig include YAML::Serializable property user : String property password : String property host : String property port : Int32 property dbname : String end struct SocketBindingConfig include YAML::Serializable property path : String property permissions : String end struct ConfigPreferences include YAML::Serializable property annotations : Bool = false property annotations_subscribed : Bool = false property preload : Bool = true property autoplay : Bool = false property captions : Array(String) = ["", "", ""] property comments : Array(String) = ["youtube", ""] property continue : Bool = false property continue_autoplay : Bool = true property dark_mode : String = "" property latest_only : Bool = false property listen : Bool = false property local : Bool = false property locale : String = "en-US" property watch_history : Bool = true property max_results : Int32 = 40 property notifications_only : Bool = false property player_style : String = "invidious" property quality : String = "dash" property quality_dash : String = "auto" property default_home : String? = "Popular" property feed_menu : Array(String) = ["Popular", "Trending", "Subscriptions", "Playlists", "History"] property automatic_instance_redirect : Bool = false property region : String = "US" property related_videos : Bool = true property sort : String = "published" property speed : Float32 = 1.0_f32 property thin_mode : Bool = false property unseen_only : Bool = false property video_loop : Bool = false property extend_desc : Bool = false property volume : Int32 = 100 property vr_mode : Bool = true property show_nick : Bool = true property save_player_pos : Bool = false @[YAML::Field(ignore: true)] property default_playlist : String? = nil property enable_dearrow : Bool = false @[YAML::Field(ignore: true)] property hidden_channels : Array(String)? = nil @[YAML::Field(ignore: true)] property default_trending_type : Invidious::Routes::Feeds::TrendingTypes = Invidious::Routes::Feeds::TrendingTypes::Default def to_tuple {% begin %} { {{(@type.instance_vars.map { |var| "#{var.name}: #{var.name}".id }).splat}} } {% end %} end end struct HTTPProxyConfig include YAML::Serializable property user : String property password : String property host : String property port : Int32 end class Config include YAML::Serializable class CompanionConfig include YAML::Serializable @[YAML::Field(converter: Preferences::URIConverter)] property private_url : URI = URI.parse("") @[YAML::Field(converter: Preferences::URIConverter)] property public_url : URI = URI.parse("") @[YAML::Field(converter: Preferences::URIConverter)] property i2p_public_url : URI = URI.parse("") property note : String = "" property domain : Array(String) = [] of String # Indicates if this companion instance uses the built-in proxy property builtin_proxy : Bool = false end # Number of threads to use for crawling videos from channels (for updating subscriptions) property channel_threads : Int32 = 1 # Time interval between two executions of the job that crawls channel videos (subscriptions update). @[YAML::Field(converter: Preferences::TimeSpanConverter)] property channel_refresh_interval : Time::Span = 30.minutes # Log file path or STDOUT property output : String = "STDOUT" # Default log level, valid YAML values are ints and strings, see src/invidious/helpers/logger.cr property log_level : LogLevel = LogLevel::Info # Enables colors in logs. Useful for debugging purposes property colorize_logs : Bool = false # Database configuration with separate parameters (username, hostname, etc) property db : DBConfig? = nil # Database configuration using 12-Factor "Database URL" syntax @[YAML::Field(converter: Preferences::URIConverter)] property database_url : URI = URI.parse("") @[YAML::Field(converter: Preferences::URIConverter)] property redis_url : URI = URI.parse("") # Use polling to keep decryption function up to date property decrypt_polling : Bool = false # Used for crawling channels: threads should check all videos uploaded by a channel property full_refresh : Bool = false # Jobs config structure. See jobs.cr and jobs/base_job.cr property jobs = Invidious::Jobs::JobsConfig.new # Used to tell Invidious it is behind a proxy, so links to resources should be https:// property https_only : Bool? # Enable or disable CSP property csp : Bool? = true # 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? # Materialious redirects property materialious_domain : String? # Subscribe to channels using PubSubHubbub (requires domain, hmac_key) property use_pubsub_feeds : Bool | Int32 = false property use_innertube_for_feeds : Bool = true property popular_enabled : Bool = true property captcha_enabled : Bool = true property login_enabled : Bool = true property registration_enabled : Bool = true property statistics_enabled : Bool = false property admins : Array(String) = [] of String property external_port : Int32? = nil property default_user_preferences : ConfigPreferences = ConfigPreferences.from_yaml("") # For compliance with DMCA, disables download widget using list of video IDs property dmca_content : Array(String) = [] of String # Check table integrity, automatically try to add any missing columns, create tables, etc. property check_tables : Bool = false # Cache annotations requested from IA, will not cache empty annotations or annotations that only contain cards property cache_annotations : Bool = false # Enables 'Strict-Transport-Security'. Ensure that `domain` and all subdomains are served securely property hsts : Bool? = true # Disable proxying server-wide: options: 'dash', 'livestreams', 'downloads', 'local' property disable_proxy : Bool? | Array(String)? = false # Enable the user notifications for all users property enable_user_notifications : Bool = true # Optional banner to be displayed along top of page for announcements, etc. property banner : String? = nil # Optional footer text to be displayed within Invidious' footer. Can be used for maintainer contact info, etc. property footer : String? = nil # Email to contact the instance maintainer. This is used within the footer as an mailto link. property instance_maintainer_email : String? = nil # URL to the modified source code to be easily AGPL compliant # Will display in the footer property modified_source_code_url : String? = nil # Link to the terms of service of the instance (if any). Will be displayed in the footer. property footer_instance_tos_link : String? = nil # Link to the privacy policy of the instance (if any). Will be displayed in the footer. property footer_instance_privacy_policy_link : String? = nil # Instance donation URL displayed in the "Instance" section of the footer property footer_instance_donate_link : String? = nil # Custom fields to be displayed within the footer's instance section property footer_instance_section_custom_fields : Array(Array(String)) = [] of Array(String) # Connect to YouTube over 'ipv6', 'ipv4'. Will sometimes resolve fix issues with rate-limiting (see https://github.com/ytdl-org/youtube-dl/issues/21729) @[YAML::Field(converter: Preferences::FamilyConverter)] property force_resolve : Socket::Family = Socket::Family::UNSPEC # External signature solver server socket (either a path to a UNIX domain socket or ":") property signature_server : String? = nil # Port to listen for connections (overridden by command line argument) property port : Int32 = 3000 # Host to bind (overridden by command line argument) property host_binding : String = "0.0.0.0" # Path and permissions to make Invidious listen on a UNIX socket instead of a TCP port property socket_binding : SocketBindingConfig? = nil # Pool size for HTTP requests to youtube.com and ytimg.com (each domain has a separate pool of `pool_size`) property pool_size : Int32 = 100 # HTTP Proxy configuration property http_proxy : HTTPProxyConfig? = nil # Use Innertube's transcripts API instead of timedtext for closed captions property use_innertube_for_captions : Bool = false # visitor data ID for Google session property visitor_data : String? = nil # poToken for passing bot attestation property po_token : String? = nil # Invidious companion property invidious_companion : Array(CompanionConfig) = [] of CompanionConfig # Invidious companion API key property invidious_companion_key : String = "" # Invidious companion prefix for numbered domains property invidious_companion_prefix : String = "" # Saved cookies in "name1=value1; name2=value2..." format @[YAML::Field(converter: Preferences::StringToCookies)] property cookies : HTTP::Cookies = HTTP::Cookies.new # Playlist length limit property playlist_length_limit : Int32 = 500 # The max resolution the Instance can offer property max_dash_resolution : Int32? property pubsub_domain : String = "" property server_id_cookie_name : String = "COMPANION_ID" property video_cache : VideoCacheConfig = VideoCacheConfig.from_yaml("") class VideoCacheConfig include YAML::Serializable property enabled : Bool = true property backend : Int32 = 1 # Max quantity of keys that can be held on the LRU cache property lru_max_size : Int32 = 18432 # ~512MB # Compress cache with Deflate property compress : Bool = false end property check_backends_interval : Int32 = 30 property force_local : Bool = true property disable_livestreams : Bool = true property max_popular_results : Int32 = 40 property disable_video_downloads : Bool = false property backend_name_prefix : String = "Backend" property videojs : VideoJSConfig = VideoJSConfig.from_yaml("") struct VideoJSConfig include YAML::Serializable include JSON::Serializable property goal_buffer_length : Int32? = 30 property max_goal_buffer_length : Int32? = 60 end def disabled?(option) case disabled = CONFIG.disable_proxy when Bool return disabled when Array if disabled.includes? option return true else return false end else return false end end def self.reload LOGGER.info("Config: Reloading configuration") # Load config from file or YAML string env var env_config_file = "INVIDIOUS_CONFIG_FILE" env_config_yaml = "INVIDIOUS_CONFIG" config_file = ENV.has_key?(env_config_file) ? ENV.fetch(env_config_file) : "config/config.yml" config_yaml = ENV.has_key?(env_config_yaml) ? ENV.fetch(env_config_yaml) : File.read(config_file) begin config = Config.from_yaml(config_yaml) rescue ex LOGGER.error("Config: Error when reloading configuration: '#{ex.message}'") config = CONFIG end # TODO: Preserve old config and don't exit on fail {% for ivar in Config.instance_vars %} CONFIG.{{ivar}} = config.{{ivar}} {% env_id = "INVIDIOUS_#{ivar.id.upcase}" %} if ENV.has_key?({{env_id}}) env_value = ENV.fetch({{env_id}}) success = false # Use YAML converter if specified {% ann = ivar.annotation(::YAML::Field) %} {% if ann && ann[:converter] %} CONFIG.{{ivar.id}} = {{ann[:converter]}}.from_yaml(YAML::ParseContext.new, YAML::Nodes.parse(ENV.fetch({{env_id}})).nodes[0]) success = true # Use regular YAML parser otherwise {% else %} {% ivar_types = ivar.type.union? ? ivar.type.union_types : [ivar.type] %} # Sort types to avoid parsing nulls and numbers as strings {% ivar_types = ivar_types.sort_by { |ivar_type| ivar_type == Nil ? 0 : ivar_type == Int32 ? 1 : 2 } %} {{ivar_types}}.each do |ivar_type| if !success begin CONFIG.{{ivar.id}} = ivar_type.from_yaml(env_value) success = true rescue # nop end end end {% end %} # Exit on fail if !success LOGGER.error("Config: Error when reloading environment variables for the configuration, exiting (fixme!)") exit(1) end end {% end %} LOGGER.info("Config: Reload successfull") end def self.load # Load config from file or YAML string env var env_config_file = "INVIDIOUS_CONFIG_FILE" env_config_yaml = "INVIDIOUS_CONFIG" config_file = ENV.has_key?(env_config_file) ? ENV.fetch(env_config_file) : "config/config.yml" config_yaml = ENV.has_key?(env_config_yaml) ? ENV.fetch(env_config_yaml) : File.read(config_file) config = Config.from_yaml(config_yaml) # Update config from env vars (upcased and prefixed with "INVIDIOUS_") # # Also checks if any top-level config options are set to "CHANGE_ME!!" # TODO: Support non-top-level config options such as the ones in DBConfig {% for ivar in Config.instance_vars %} {% env_id = "INVIDIOUS_#{ivar.id.upcase}" %} if ENV.has_key?({{env_id}}) env_value = ENV.fetch({{env_id}}) success = false # Use YAML converter if specified {% ann = ivar.annotation(::YAML::Field) %} {% if ann && ann[:converter] %} config.{{ivar.id}} = {{ann[:converter]}}.from_yaml(YAML::ParseContext.new, YAML::Nodes.parse(ENV.fetch({{env_id}})).nodes[0]) success = true # Use regular YAML parser otherwise {% else %} {% ivar_types = ivar.type.union? ? ivar.type.union_types : [ivar.type] %} # Sort types to avoid parsing nulls and numbers as strings {% ivar_types = ivar_types.sort_by { |ivar_type| ivar_type == Nil ? 0 : ivar_type == Int32 ? 1 : 2 } %} {{ivar_types}}.each do |ivar_type| if !success begin config.{{ivar.id}} = ivar_type.from_yaml(env_value) success = true rescue # nop end end end {% end %} # Exit on fail if !success puts %(Config.{{ivar.id}} failed to parse #{env_value} as {{ivar.type}}) exit(1) end end # Warn when any config attribute is set to "CHANGE_ME!!" if config.{{ivar.id}} == "CHANGE_ME!!" puts "Config: The value of '#{ {{ivar.stringify}} }' needs to be changed!!" exit(1) end {% end %} if config.invidious_companion.present? # invidious_companion and signature_server can't work together if config.signature_server puts "Config: You can not run inv_sig_helper and invidious_companion at the same time." exit(1) elsif config.invidious_companion_key.empty? puts "Config: Please configure a key if you are using invidious companion." exit(1) elsif config.invidious_companion_key == "CHANGE_ME!!" puts "Config: The value of 'invidious_companion_key' needs to be changed!!" exit(1) elsif config.invidious_companion_key.size != 16 puts "Config: The value of 'invidious_companion_key' needs to be a size of 16 characters." exit(1) end # Set public_url to built-in proxy path when omitted config.invidious_companion.each do |companion| if companion.public_url.to_s.empty? companion.public_url = URI.parse("/companion") companion.builtin_proxy = true end end elsif config.signature_server puts("WARNING: inv-sig-helper is deprecated. Please switch to Invidious companion: https://docs.invidious.io/installation/") else puts("WARNING: Invidious companion is required to view and playback videos. For more information see https://docs.invidious.io/installation/") end # HMAC_key is mandatory # See: https://github.com/iv-org/invidious/issues/3854 if config.hmac_key.empty? puts "Config: 'hmac_key' is required/can't be empty" exit(1) end # Build database_url from db.* if it's not set directly if config.database_url.to_s.empty? if db = config.db config.database_url = URI.new( scheme: "postgres", user: db.user, password: db.password, host: db.host, port: db.port, path: db.dbname, ) else puts "Config: Either database_url or db.* is required" exit(1) end end if config.video_cache.enabled if !config.video_cache.backend.in?(0, 1, 2) puts "Config: 'video_cache_storage', can only be:" puts "0 (PostgreSQL)" puts "1 (Redis compatible DB) (Default)" puts "2 (In memory LRU)" end if config.video_cache.compress && config.video_cache.backend == 0 puts "Video Cache compression can only be enabled when using backend 1 (Redis) or 2 (LRU)" exit(1) end end # Check if the socket configuration is valid if sb = config.socket_binding if sb.path.ends_with?("/") || File.directory?(sb.path) puts "Config: The socket path " + sb.path + " must not be a directory!" exit(1) end d = File.dirname(sb.path) if !File.directory?(d) puts "Config: Socket directory " + sb.path + " does not exist or is not a directory!" exit(1) end p = sb.permissions.to_i?(base: 8) if !p || p < 0 || p > 0o777 puts "Config: Socket permissions must be an octal between 0 and 777!" exit(1) end end return config end end