summaryrefslogtreecommitdiffstats
path: root/src/invidious/config.cr
blob: 453256b52a8852551e842527a9618da130bbfe93 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
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 = "hd720"
  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

  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

  # 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?
  # 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

  # External signature solver server socket (either a path to a UNIX domain socket or "<IP>:<Port>")
  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

  # 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

  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 %}

    # 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

    # 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