mirror of
https://github.com/iv-org/invidious.git
synced 2026-08-16 21:10:53 -05:00
400 lines
15 KiB
Crystal
400 lines
15 KiB
Crystal
struct DBConfig
|
|
include YAML::Serializable
|
|
|
|
property user : String
|
|
property password : String
|
|
property host : String
|
|
property port : Int32
|
|
property dbname : String
|
|
|
|
# How many connections to construct on start-up, and keep it there.
|
|
property initial_pool_size : Int32 = 1
|
|
# The maximum size of the connection pool
|
|
property max_pool_size : Int32 = 100
|
|
# The maximum amount of idle connections within the pool
|
|
# idle connections are defined as created connections that are
|
|
# sitting around in the pool. Exceeding this number will cause new connections
|
|
# to be created on checkout and then simply dropped on release, till the maximum pool size
|
|
# from which there will be a checkout timeout.
|
|
property max_idle_pool_size : Int32 = 100
|
|
# The maximum amount of seconds to wait for a connection to become
|
|
# available when all connections can be checked out, and the pool has
|
|
# reached its maximum size.
|
|
property checkout_timeout : Float32 = 5.0
|
|
# The number of tries allowed to establish a connection, reconnect, or retry
|
|
# the command in case of any network errors.
|
|
property retry_attempts : Int32 = 5
|
|
# The number of seconds between each retry
|
|
property retry_delay : Float32 = 1.0
|
|
|
|
def to_url
|
|
URI.new(
|
|
scheme: "postgres",
|
|
user: user,
|
|
password: password,
|
|
host: host,
|
|
port: port,
|
|
path: dbname,
|
|
query: get_connection_pool_query_string
|
|
)
|
|
end
|
|
|
|
# Creates the query parameters for configuring the connection pool
|
|
private def get_connection_pool_query_string
|
|
{% begin %}
|
|
{% pool_vars = @type.instance_vars.reject { |v| {"user", "password", "host", "port", "dbname"}.includes?(v.name.stringify) } %}
|
|
{% raise "Error unable to isolate database connection pool properties" if pool_vars.size > 6 %}
|
|
|
|
URI::Params.build do | build |
|
|
{% for vars in pool_vars %}
|
|
build.add {{vars.name.stringify}}, {{vars.name}}.to_s
|
|
{% end %}
|
|
end
|
|
{% end %}
|
|
end
|
|
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"]
|
|
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 search_privacy : Bool = false
|
|
|
|
def to_tuple
|
|
{% begin %}
|
|
{
|
|
{{(@type.instance_vars.map { |var| "#{var.name}: #{var.name}".id }).splat}}
|
|
}
|
|
{% end %}
|
|
end
|
|
end
|
|
|
|
# Outbound proxy configuration. The name is kept for config backwards
|
|
# compatibility; `type` selects the protocol (HTTP CONNECT or SOCKS5).
|
|
struct HTTPProxyConfig
|
|
include YAML::Serializable
|
|
|
|
# Proxy protocol: "http" (HTTP CONNECT, the default), "socks5", or "socks5h".
|
|
# SOCKS5 resolves target hostnames on the proxy side (SOCKS5h semantics).
|
|
property type : String = "http"
|
|
# Credentials are optional: omit both for an unauthenticated proxy.
|
|
property user : String? = nil
|
|
property password : String? = nil
|
|
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("")
|
|
|
|
# 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
|
|
# Number of threads to use for updating feeds
|
|
property feed_threads : Int32 = 1
|
|
# 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("")
|
|
# 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?
|
|
# 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?
|
|
# Additional domain list that is going to be used for cookie domain validation
|
|
property alternative_domains : Array(String) = [] of String
|
|
# Subscribe to channels using PubSubHubbub (requires domain, hmac_key)
|
|
property use_pubsub_feeds : Bool | Int32 = false
|
|
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
|
|
# Optional banner to be displayed along top of page for announcements, etc.
|
|
property banner : String? = nil
|
|
# 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
|
|
|
|
# URL to the modified source code to be easily AGPL compliant
|
|
# Will display in the footer, next to the main source code link
|
|
property modified_source_code_url : String? = nil
|
|
|
|
# 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
|
|
|
|
# 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
|
|
|
|
# Invidious companion
|
|
property invidious_companion : Array(CompanionConfig) = [] of CompanionConfig
|
|
|
|
# Invidious companion API key
|
|
property invidious_companion_key : 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
|
|
|
|
# Disable easy to abuse API endpoints
|
|
property disable_abusable_api : Bool = false
|
|
|
|
property videojs : VideoJSConfig = VideoJSConfig.from_yaml("")
|
|
|
|
struct VideoJSConfig
|
|
include YAML::Serializable
|
|
include JSON::Serializable
|
|
|
|
# This are the default values that VideoJS uses.
|
|
# See `assets/videojs/video.js/video.js` file
|
|
# and search for `GOAL_BUFFER_LENGTH` and `MAX_GOAL_BUFFER_LENGTH`
|
|
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.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?
|
|
if 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
|
|
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 = db.to_url
|
|
else
|
|
puts "Config: Either database_url or db.* is required"
|
|
exit(1)
|
|
end
|
|
else
|
|
# Add default connection pool settings as needed
|
|
db_url_query_params = config.database_url.query_params
|
|
|
|
{% begin %}
|
|
{% pool_vars = DBConfig.instance_vars.reject { |v| {"user", "password", "host", "port", "dbname"}.includes?(v.name.stringify) } %}
|
|
{% for vars in pool_vars %}
|
|
db_url_query_params[{{vars.name.stringify}}] = db_url_query_params[{{vars.name.stringify}}]? || {{vars.default_value}}.to_s
|
|
{% end %}
|
|
{% end %}
|
|
|
|
config.database_url.query_params = db_url_query_params
|
|
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
|