summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--spec/helpers_spec.cr1
-rw-r--r--src/invidious.cr1
-rw-r--r--src/invidious/config.cr190
-rw-r--r--src/invidious/helpers/helpers.cr191
-rw-r--r--src/invidious/helpers/utils.cr13
-rw-r--r--src/invidious/user/converters.cr12
-rw-r--r--src/invidious/user/preferences.cr257
-rw-r--r--src/invidious/users.cr258
8 files changed, 460 insertions, 463 deletions
diff --git a/spec/helpers_spec.cr b/spec/helpers_spec.cr
index b17c8d73..002c8bdd 100644
--- a/spec/helpers_spec.cr
+++ b/spec/helpers_spec.cr
@@ -11,7 +11,6 @@ require "../src/invidious/comments"
require "../src/invidious/playlists"
require "../src/invidious/search"
require "../src/invidious/trending"
-require "../src/invidious/users"
CONFIG = Config.from_yaml(File.open("config/config.example.yml"))
diff --git a/src/invidious.cr b/src/invidious.cr
index 3a20b0d8..570c33e6 100644
--- a/src/invidious.cr
+++ b/src/invidious.cr
@@ -30,6 +30,7 @@ require "./invidious/helpers/*"
require "./invidious/yt_backend/*"
require "./invidious/*"
require "./invidious/channels/*"
+require "./invidious/user/*"
require "./invidious/routes/**"
require "./invidious/jobs/**"
diff --git a/src/invidious/config.cr b/src/invidious/config.cr
new file mode 100644
index 00000000..e2bc5722
--- /dev/null
+++ b/src/invidious/config.cr
@@ -0,0 +1,190 @@
+struct DBConfig
+ include YAML::Serializable
+
+ property user : String
+ property password : String
+ property host : String
+ property port : Int32
+ property dbname : String
+end
+
+struct ConfigPreferences
+ include YAML::Serializable
+
+ property annotations : Bool = false
+ property annotations_subscribed : Bool = false
+ 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 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 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
+
+ def to_tuple
+ {% begin %}
+ {
+ {{*@type.instance_vars.map { |var| "#{var.name}: #{var.name}".id }}}
+ }
+ {% end %}
+ end
+end
+
+class Config
+ include YAML::Serializable
+
+ property channel_threads : Int32 = 1 # Number of threads to use for crawling videos from channels (for updating subscriptions)
+ property feed_threads : Int32 = 1 # Number of threads to use for updating feeds
+ property output : String = "STDOUT" # Log file path or STDOUT
+ property log_level : LogLevel = LogLevel::Info # Default log level, valid YAML values are ints and strings, see src/invidious/helpers/logger.cr
+ property db : DBConfig? = nil # Database configuration with separate parameters (username, hostname, etc)
+
+ @[YAML::Field(converter: Preferences::URIConverter)]
+ property database_url : URI = URI.parse("") # Database configuration using 12-Factor "Database URL" syntax
+ property decrypt_polling : Bool = true # Use polling to keep decryption function up to date
+ property full_refresh : Bool = false # Used for crawling channels: threads should check all videos uploaded by a channel
+ property https_only : Bool? # Used to tell Invidious it is behind a proxy, so links to resources should be https://
+ property hmac_key : String? # HMAC signing key for CSRF tokens and verifying pubsub subscriptions
+ property domain : String? # Domain to be used for links to resources on the site where an absolute URL is required
+ property use_pubsub_feeds : Bool | Int32 = false # Subscribe to channels using PubSubHubbub (requires domain, hmac_key)
+ 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("")
+ property dmca_content : Array(String) = [] of String # For compliance with DMCA, disables download widget using list of video IDs
+ property check_tables : Bool = false # Check table integrity, automatically try to add any missing columns, create tables, etc.
+ property cache_annotations : Bool = false # Cache annotations requested from IA, will not cache empty annotations or annotations that only contain cards
+ property banner : String? = nil # Optional banner to be displayed along top of page for announcements, etc.
+ property hsts : Bool? = true # Enables 'Strict-Transport-Security'. Ensure that `domain` and all subdomains are served securely
+ property disable_proxy : Bool? | Array(String)? = false # Disable proxying server-wide: options: 'dash', 'livestreams', 'downloads', 'local'
+
+ # 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
+
+ @[YAML::Field(converter: Preferences::FamilyConverter)]
+ property force_resolve : Socket::Family = Socket::Family::UNSPEC # Connect to YouTube over 'ipv6', 'ipv4'. Will sometimes resolve fix issues with rate-limiting (see https://github.com/ytdl-org/youtube-dl/issues/21729)
+ property port : Int32 = 3000 # Port to listen for connections (overrided by command line argument)
+ property host_binding : String = "0.0.0.0" # Host to bind (overrided by command line argument)
+ property pool_size : Int32 = 100 # Pool size for HTTP requests to youtube.com and ytimg.com (each domain has a separate pool of `pool_size`)
+ property use_quic : Bool = true # Use quic transport for youtube api
+
+ @[YAML::Field(converter: Preferences::StringToCookies)]
+ property cookies : HTTP::Cookies = HTTP::Cookies.new # Saved cookies in "name1=value1; name2=value2..." format
+ property captcha_key : String? = nil # Key for Anti-Captcha
+ property captcha_api_url : String = "https://api.anti-captcha.com" # API URL for Anti-Captcha
+
+ 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_")
+ {% for ivar in Config.instance_vars %}
+ {% env_id = "INVIDIOUS_#{ivar.id.upcase}" %}
+
+ if ENV.has_key?({{env_id}})
+ # puts %(Config.{{ivar.id}} : Loading from env var {{env_id}})
+ env_value = ENV.fetch({{env_id}})
+ success = false
+
+ # Use YAML converter if specified
+ {% ann = ivar.annotation(::YAML::Field) %}
+ {% if ann && ann[:converter] %}
+ puts %(Config.{{ivar.id}} : Parsing "#{env_value}" as {{ivar.type}} with {{ann[:converter]}} converter)
+ config.{{ivar.id}} = {{ann[:converter]}}.from_yaml(YAML::ParseContext.new, YAML::Nodes.parse(ENV.fetch({{env_id}})).nodes[0])
+ puts %(Config.{{ivar.id}} : Set to #{config.{{ivar.id}}})
+ 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
+ # puts %(Config.{{ivar.id}} : Trying to parse "#{env_value}" as #{ivar_type})
+ config.{{ivar.id}} = ivar_type.from_yaml(env_value)
+ puts %(Config.{{ivar.id}} : Set to #{config.{{ivar.id}}} (#{ivar_type}))
+ 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
+ {% 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
+
+ return config
+ end
+end
diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr
index c01ca11e..2e61d21f 100644
--- a/src/invidious/helpers/helpers.cr
+++ b/src/invidious/helpers/helpers.cr
@@ -22,197 +22,6 @@ struct Annotation
property annotations : String
end
-struct ConfigPreferences
- include YAML::Serializable
-
- property annotations : Bool = false
- property annotations_subscribed : Bool = false
- 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 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 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
-
- def to_tuple
- {% begin %}
- {
- {{*@type.instance_vars.map { |var| "#{var.name}: #{var.name}".id }}}
- }
- {% end %}
- end
-end
-
-class Config
- include YAML::Serializable
-
- property channel_threads : Int32 = 1 # Number of threads to use for crawling videos from channels (for updating subscriptions)
- property feed_threads : Int32 = 1 # Number of threads to use for updating feeds
- property output : String = "STDOUT" # Log file path or STDOUT
- property log_level : LogLevel = LogLevel::Info # Default log level, valid YAML values are ints and strings, see src/invidious/helpers/logger.cr
- property db : DBConfig? = nil # Database configuration with separate parameters (username, hostname, etc)
-
- @[YAML::Field(converter: Preferences::URIConverter)]
- property database_url : URI = URI.parse("") # Database configuration using 12-Factor "Database URL" syntax
- property decrypt_polling : Bool = true # Use polling to keep decryption function up to date
- property full_refresh : Bool = false # Used for crawling channels: threads should check all videos uploaded by a channel
- property https_only : Bool? # Used to tell Invidious it is behind a proxy, so links to resources should be https://
- property hmac_key : String? # HMAC signing key for CSRF tokens and verifying pubsub subscriptions
- property domain : String? # Domain to be used for links to resources on the site where an absolute URL is required
- property use_pubsub_feeds : Bool | Int32 = false # Subscribe to channels using PubSubHubbub (requires domain, hmac_key)
- 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("")
- property dmca_content : Array(String) = [] of String # For compliance with DMCA, disables download widget using list of video IDs
- property check_tables : Bool = false # Check table integrity, automatically try to add any missing columns, create tables, etc.
- property cache_annotations : Bool = false # Cache annotations requested from IA, will not cache empty annotations or annotations that only contain cards
- property banner : String? = nil # Optional banner to be displayed along top of page for announcements, etc.
- property hsts : Bool? = true # Enables 'Strict-Transport-Security'. Ensure that `domain` and all subdomains are served securely
- property disable_proxy : Bool? | Array(String)? = false # Disable proxying server-wide: options: 'dash', 'livestreams', 'downloads', 'local'
-
- # 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
-
- @[YAML::Field(converter: Preferences::FamilyConverter)]
- property force_resolve : Socket::Family = Socket::Family::UNSPEC # Connect to YouTube over 'ipv6', 'ipv4'. Will sometimes resolve fix issues with rate-limiting (see https://github.com/ytdl-org/youtube-dl/issues/21729)
- property port : Int32 = 3000 # Port to listen for connections (overrided by command line argument)
- property host_binding : String = "0.0.0.0" # Host to bind (overrided by command line argument)
- property pool_size : Int32 = 100 # Pool size for HTTP requests to youtube.com and ytimg.com (each domain has a separate pool of `pool_size`)
- property use_quic : Bool = true # Use quic transport for youtube api
-
- @[YAML::Field(converter: Preferences::StringToCookies)]
- property cookies : HTTP::Cookies = HTTP::Cookies.new # Saved cookies in "name1=value1; name2=value2..." format
- property captcha_key : String? = nil # Key for Anti-Captcha
- property captcha_api_url : String = "https://api.anti-captcha.com" # API URL for Anti-Captcha
-
- 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_")
- {% for ivar in Config.instance_vars %}
- {% env_id = "INVIDIOUS_#{ivar.id.upcase}" %}
-
- if ENV.has_key?({{env_id}})
- # puts %(Config.{{ivar.id}} : Loading from env var {{env_id}})
- env_value = ENV.fetch({{env_id}})
- success = false
-
- # Use YAML converter if specified
- {% ann = ivar.annotation(::YAML::Field) %}
- {% if ann && ann[:converter] %}
- puts %(Config.{{ivar.id}} : Parsing "#{env_value}" as {{ivar.type}} with {{ann[:converter]}} converter)
- config.{{ivar.id}} = {{ann[:converter]}}.from_yaml(YAML::ParseContext.new, YAML::Nodes.parse(ENV.fetch({{env_id}})).nodes[0])
- puts %(Config.{{ivar.id}} : Set to #{config.{{ivar.id}}})
- 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
- # puts %(Config.{{ivar.id}} : Trying to parse "#{env_value}" as #{ivar_type})
- config.{{ivar.id}} = ivar_type.from_yaml(env_value)
- puts %(Config.{{ivar.id}} : Set to #{config.{{ivar.id}}} (#{ivar_type}))
- 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
- {% 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
-
- return config
- end
-end
-
-struct DBConfig
- include YAML::Serializable
-
- property user : String
- property password : String
- property host : String
- property port : Int32
- property dbname : String
-end
-
def login_req(f_req)
data = {
# Unfortunately there's not much information available on `bgRequest`; part of Google's BotGuard
diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr
index 65067526..603b4e1f 100644
--- a/src/invidious/helpers/utils.cr
+++ b/src/invidious/helpers/utils.cr
@@ -301,19 +301,6 @@ def parse_range(range)
return 0_i64, nil
end
-def convert_theme(theme)
- case theme
- when "true"
- "dark"
- when "false"
- "light"
- when "", nil
- nil
- else
- theme
- end
-end
-
def fetch_random_instance
begin
instance_api_client = make_client(URI.parse("https://api.invidious.io"))
diff --git a/src/invidious/user/converters.cr b/src/invidious/user/converters.cr
new file mode 100644
index 00000000..dcbf8c53
--- /dev/null
+++ b/src/invidious/user/converters.cr
@@ -0,0 +1,12 @@
+def convert_theme(theme)
+ case theme
+ when "true"
+ "dark"
+ when "false"
+ "light"
+ when "", nil
+ nil
+ else
+ theme
+ end
+end
diff --git a/src/invidious/user/preferences.cr b/src/invidious/user/preferences.cr
new file mode 100644
index 00000000..453a635e
--- /dev/null
+++ b/src/invidious/user/preferences.cr
@@ -0,0 +1,257 @@
+struct Preferences
+ include JSON::Serializable
+ include YAML::Serializable
+
+ property annotations : Bool = CONFIG.default_user_preferences.annotations
+ property annotations_subscribed : Bool = CONFIG.default_user_preferences.annotations_subscribed
+ property autoplay : Bool = CONFIG.default_user_preferences.autoplay
+ property automatic_instance_redirect : Bool = CONFIG.default_user_preferences.automatic_instance_redirect
+
+ @[JSON::Field(converter: Preferences::StringToArray)]
+ @[YAML::Field(converter: Preferences::StringToArray)]
+ property captions : Array(String) = CONFIG.default_user_preferences.captions
+
+ @[JSON::Field(converter: Preferences::StringToArray)]
+ @[YAML::Field(converter: Preferences::StringToArray)]
+ property comments : Array(String) = CONFIG.default_user_preferences.comments
+ property continue : Bool = CONFIG.default_user_preferences.continue
+ property continue_autoplay : Bool = CONFIG.default_user_preferences.continue_autoplay
+
+ @[JSON::Field(converter: Preferences::BoolToString)]
+ @[YAML::Field(converter: Preferences::BoolToString)]
+ property dark_mode : String = CONFIG.default_user_preferences.dark_mode
+ property latest_only : Bool = CONFIG.default_user_preferences.latest_only
+ property listen : Bool = CONFIG.default_user_preferences.listen
+ property local : Bool = CONFIG.default_user_preferences.local
+ property vr_mode : Bool = CONFIG.default_user_preferences.vr_mode
+ property show_nick : Bool = CONFIG.default_user_preferences.show_nick
+
+ @[JSON::Field(converter: Preferences::ProcessString)]
+ property locale : String = CONFIG.default_user_preferences.locale
+
+ @[JSON::Field(converter: Preferences::ClampInt)]
+ property max_results : Int32 = CONFIG.default_user_preferences.max_results
+ property notifications_only : Bool = CONFIG.default_user_preferences.notifications_only
+
+ @[JSON::Field(converter: Preferences::ProcessString)]
+ property player_style : String = CONFIG.default_user_preferences.player_style
+
+ @[JSON::Field(converter: Preferences::ProcessString)]
+ property quality : String = CONFIG.default_user_preferences.quality
+ @[JSON::Field(converter: Preferences::ProcessString)]
+ property quality_dash : String = CONFIG.default_user_preferences.quality_dash
+ property default_home : String? = CONFIG.default_user_preferences.default_home
+ property feed_menu : Array(String) = CONFIG.default_user_preferences.feed_menu
+ property related_videos : Bool = CONFIG.default_user_preferences.related_videos
+
+ @[JSON::Field(converter: Preferences::ProcessString)]
+ property sort : String = CONFIG.default_user_preferences.sort
+ property speed : Float32 = CONFIG.default_user_preferences.speed
+ property thin_mode : Bool = CONFIG.default_user_preferences.thin_mode
+ property unseen_only : Bool = CONFIG.default_user_preferences.unseen_only
+ property video_loop : Bool = CONFIG.default_user_preferences.video_loop
+ property extend_desc : Bool = CONFIG.default_user_preferences.extend_desc
+ property volume : Int32 = CONFIG.default_user_preferences.volume
+
+ module BoolToString
+ def self.to_json(value : String, json : JSON::Builder)
+ json.string value
+ end
+
+ def self.from_json(value : JSON::PullParser) : String
+ begin
+ result = value.read_string
+
+ if result.empty?
+ CONFIG.default_user_preferences.dark_mode
+ else
+ result
+ end
+ rescue ex
+ if value.read_bool
+ "dark"
+ else
+ "light"
+ end
+ end
+ end
+
+ def self.to_yaml(value : String, yaml : YAML::Nodes::Builder)
+ yaml.scalar value
+ end
+
+ def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : String
+ unless node.is_a?(YAML::Nodes::Scalar)
+ node.raise "Expected scalar, not #{node.class}"
+ end
+
+ case node.value
+ when "true"
+ "dark"
+ when "false"
+ "light"
+ when ""
+ CONFIG.default_user_preferences.dark_mode
+ else
+ node.value
+ end
+ end
+ end
+
+ module ClampInt
+ def self.to_json(value : Int32, json : JSON::Builder)
+ json.number value
+ end
+
+ def self.from_json(value : JSON::PullParser) : Int32
+ value.read_int.clamp(0, MAX_ITEMS_PER_PAGE).to_i32
+ end
+
+ def self.to_yaml(value : Int32, yaml : YAML::Nodes::Builder)
+ yaml.scalar value
+ end
+
+ def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : Int32
+ node.value.clamp(0, MAX_ITEMS_PER_PAGE)
+ end
+ end
+
+ module FamilyConverter
+ def self.to_yaml(value : Socket::Family, yaml : YAML::Nodes::Builder)
+ case value
+ when Socket::Family::UNSPEC
+ yaml.scalar nil
+ when Socket::Family::INET
+ yaml.scalar "ipv4"
+ when Socket::Family::INET6
+ yaml.scalar "ipv6"
+ when Socket::Family::UNIX
+ raise "Invalid socket family #{value}"
+ end
+ end
+
+ def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : Socket::Family
+ if node.is_a?(YAML::Nodes::Scalar)
+ case node.value.downcase
+ when "ipv4"
+ Socket::Family::INET
+ when "ipv6"
+ Socket::Family::INET6
+ else
+ Socket::Family::UNSPEC
+ end
+ else
+ node.raise "Expected scalar, not #{node.class}"
+ end
+ end
+ end
+
+ module URIConverter
+ def self.to_yaml(value : URI, yaml : YAML::Nodes::Builder)
+ yaml.scalar value.normalize!
+ end
+
+ def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : URI
+ if node.is_a?(YAML::Nodes::Scalar)
+ URI.parse node.value
+ else
+ node.raise "Expected scalar, not #{node.class}"
+ end
+ end
+ end
+
+ module ProcessString
+ def self.to_json(value : String, json : JSON::Builder)
+ json.string value
+ end
+
+ def self.from_json(value : JSON::PullParser) : String
+ HTML.escape(value.read_string[0, 100])
+ end
+
+ def self.to_yaml(value : String, yaml : YAML::Nodes::Builder)
+ yaml.scalar value
+ end
+
+ def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : String
+ HTML.escape(node.value[0, 100])
+ end
+ end
+
+ module StringToArray
+ def self.to_json(value : Array(String), json : JSON::Builder)
+ json.array do
+ value.each do |element|
+ json.string element
+ end
+ end
+ end
+
+ def self.from_json(value : JSON::PullParser) : Array(String)
+ begin
+ result = [] of String
+ value.read_array do
+ result << HTML.escape(value.read_string[0, 100])
+ end
+ rescue ex
+ result = [HTML.escape(value.read_string[0, 100]), ""]
+ end
+
+ result
+ end
+
+ def self.to_yaml(value : Array(String), yaml : YAML::Nodes::Builder)
+ yaml.sequence do
+ value.each do |element|
+ yaml.scalar element
+ end
+ end
+ end
+
+ def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : Array(String)
+ begin
+ unless node.is_a?(YAML::Nodes::Sequence)
+ node.raise "Expected sequence, not #{node.class}"
+ end
+
+ result = [] of String
+ node.nodes.each do |item|
+ unless item.is_a?(YAML::Nodes::Scalar)
+ node.raise "Expected scalar, not #{item.class}"
+ end
+
+ result << HTML.escape(item.value[0, 100])
+ end
+ rescue ex
+ if node.is_a?(YAML::Nodes::Scalar)
+ result = [HTML.escape(node.value[0, 100]), ""]
+ else
+ result = ["", ""]
+ end
+ end
+
+ result
+ end
+ end
+
+ module StringToCookies
+ def self.to_yaml(value : HTTP::Cookies, yaml : YAML::Nodes::Builder)
+ (value.map { |c| "#{c.name}=#{c.value}" }).join("; ").to_yaml(yaml)
+ end
+
+ def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : HTTP::Cookies
+ unless node.is_a?(YAML::Nodes::Scalar)
+ node.raise "Expected scalar, not #{node.class}"
+ end
+
+ cookies = HTTP::Cookies.new
+ node.value.split(";").each do |cookie|
+ next if cookie.strip.empty?
+ name, value = cookie.split("=", 2)
+ cookies << HTTP::Cookie.new(name.strip, value.strip)
+ end
+
+ cookies
+ end
+ end
+end
diff --git a/src/invidious/users.cr b/src/invidious/users.cr
index aff76b53..8ea7bd4a 100644
--- a/src/invidious/users.cr
+++ b/src/invidious/users.cr
@@ -29,264 +29,6 @@ struct User
end
end
-struct Preferences
- include JSON::Serializable
- include YAML::Serializable
-
- property annotations : Bool = CONFIG.default_user_preferences.annotations
- property annotations_subscribed : Bool = CONFIG.default_user_preferences.annotations_subscribed
- property autoplay : Bool = CONFIG.default_user_preferences.autoplay
- property automatic_instance_redirect : Bool = CONFIG.default_user_preferences.automatic_instance_redirect
-
- @[JSON::Field(converter: Preferences::StringToArray)]
- @[YAML::Field(converter: Preferences::StringToArray)]
- property captions : Array(String) = CONFIG.default_user_preferences.captions
-
- @[JSON::Field(converter: Preferences::StringToArray)]
- @[YAML::Field(converter: Preferences::StringToArray)]
- property comments : Array(String) = CONFIG.default_user_preferences.comments
- property continue : Bool = CONFIG.default_user_preferences.continue
- property continue_autoplay : Bool = CONFIG.default_user_preferences.continue_autoplay
-
- @[JSON::Field(converter: Preferences::BoolToString)]
- @[YAML::Field(converter: Preferences::BoolToString)]
- property dark_mode : String = CONFIG.default_user_preferences.dark_mode
- property latest_only : Bool = CONFIG.default_user_preferences.latest_only
- property listen : Bool = CONFIG.default_user_preferences.listen
- property local : Bool = CONFIG.default_user_preferences.local
- property vr_mode : Bool = CONFIG.default_user_preferences.vr_mode
- property show_nick : Bool = CONFIG.default_user_preferences.show_nick
-
- @[JSON::Field(converter: Preferences::ProcessString)]
- property locale : String = CONFIG.default_user_preferences.locale
-
- @[JSON::Field(converter: Preferences::ClampInt)]
- property max_results : Int32 = CONFIG.default_user_preferences.max_results
- property notifications_only : Bool = CONFIG.default_user_preferences.notifications_only
-
- @[JSON::Field(converter: Preferences::ProcessString)]
- property player_style : String = CONFIG.default_user_preferences.player_style
-
- @[JSON::Field(converter: Preferences::ProcessString)]
- property quality : String = CONFIG.default_user_preferences.quality
- @[JSON::Field(converter: Preferences::ProcessString)]
- property quality_dash : String = CONFIG.default_user_preferences.quality_dash
- property default_home : String? = CONFIG.default_user_preferences.default_home
- property feed_menu : Array(String) = CONFIG.default_user_preferences.feed_menu
- property related_videos : Bool = CONFIG.default_user_preferences.related_videos
-
- @[JSON::Field(converter: Preferences::ProcessString)]
- property sort : String = CONFIG.default_user_preferences.sort
- property speed : Float32 = CONFIG.default_user_preferences.speed
- property thin_mode : Bool = CONFIG.default_user_preferences.thin_mode
- property unseen_only : Bool = CONFIG.default_user_preferences.unseen_only
- property video_loop : Bool = CONFIG.default_user_preferences.video_loop
- property extend_desc : Bool = CONFIG.default_user_preferences.extend_desc
- property volume : Int32 = CONFIG.default_user_preferences.volume
-
- module BoolToString
- def self.to_json(value : String, json : JSON::Builder)
- json.string value
- end
-
- def self.from_json(value : JSON::PullParser) : String
- begin
- result = value.read_string
-
- if result.empty?
- CONFIG.default_user_preferences.dark_mode
- else
- result
- end
- rescue ex
- if value.read_bool
- "dark"
- else
- "light"
- end
- end
- end
-
- def self.to_yaml(value : String, yaml : YAML::Nodes::Builder)
- yaml.scalar value
- end
-
- def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : String
- unless node.is_a?(YAML::Nodes::Scalar)
- node.raise "Expected scalar, not #{node.class}"
- end
-
- case node.value
- when "true"
- "dark"
- when "false"
- "light"
- when ""
- CONFIG.default_user_preferences.dark_mode
- else
- node.value
- end
- end
- end
-
- module ClampInt
- def self.to_json(value : Int32, json : JSON::Builder)
- json.number value
- end
-
- def self.from_json(value : JSON::PullParser) : Int32
- value.read_int.clamp(0, MAX_ITEMS_PER_PAGE).to_i32
- end
-
- def self.to_yaml(value : Int32, yaml : YAML::Nodes::Builder)
- yaml.scalar value
- end
-
- def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : Int32
- node.value.clamp(0, MAX_ITEMS_PER_PAGE)
- end
- end
-
- module FamilyConverter
- def self.to_yaml(value : Socket::Family, yaml : YAML::Nodes::Builder)
- case value
- when Socket::Family::UNSPEC
- yaml.scalar nil
- when Socket::Family::INET
- yaml.scalar "ipv4"
- when Socket::Family::INET6
- yaml.scalar "ipv6"
- when Socket::Family::UNIX
- raise "Invalid socket family #{value}"
- end
- end
-
- def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : Socket::Family
- if node.is_a?(YAML::Nodes::Scalar)
- case node.value.downcase
- when "ipv4"
- Socket::Family::INET
- when "ipv6"
- Socket::Family::INET6
- else
- Socket::Family::UNSPEC
- end
- else
- node.raise "Expected scalar, not #{node.class}"
- end
- end
- end
-
- module URIConverter
- def self.to_yaml(value : URI, yaml : YAML::Nodes::Builder)
- yaml.scalar value.normalize!
- end
-
- def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : URI
- if node.is_a?(YAML::Nodes::Scalar)
- URI.parse node.value
- else
- node.raise "Expected scalar, not #{node.class}"
- end
- end
- end
-
- module ProcessString
- def self.to_json(value : String, json : JSON::Builder)
- json.string value
- end
-
- def self.from_json(value : JSON::PullParser) : String
- HTML.escape(value.read_string[0, 100])
- end
-
- def self.to_yaml(value : String, yaml : YAML::Nodes::Builder)
- yaml.scalar value
- end
-
- def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : String
- HTML.escape(node.value[0, 100])
- end
- end
-
- module StringToArray
- def self.to_json(value : Array(String), json : JSON::Builder)
- json.array do
- value.each do |element|
- json.string element
- end
- end
- end
-
- def self.from_json(value : JSON::PullParser) : Array(String)
- begin
- result = [] of String
- value.read_array do
- result << HTML.escape(value.read_string[0, 100])
- end
- rescue ex
- result = [HTML.escape(value.read_string[0, 100]), ""]
- end
-
- result
- end
-
- def self.to_yaml(value : Array(String), yaml : YAML::Nodes::Builder)
- yaml.sequence do
- value.each do |element|
- yaml.scalar element
- end
- end
- end
-
- def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : Array(String)
- begin
- unless node.is_a?(YAML::Nodes::Sequence)
- node.raise "Expected sequence, not #{node.class}"
- end
-
- result = [] of String
- node.nodes.each do |item|
- unless item.is_a?(YAML::Nodes::Scalar)
- node.raise "Expected scalar, not #{item.class}"
- end
-
- result << HTML.escape(item.value[0, 100])
- end
- rescue ex
- if node.is_a?(YAML::Nodes::Scalar)
- result = [HTML.escape(node.value[0, 100]), ""]
- else
- result = ["", ""]
- end
- end
-
- result
- end
- end
-
- module StringToCookies
- def self.to_yaml(value : HTTP::Cookies, yaml : YAML::Nodes::Builder)
- (value.map { |c| "#{c.name}=#{c.value}" }).join("; ").to_yaml(yaml)
- end
-
- def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : HTTP::Cookies
- unless node.is_a?(YAML::Nodes::Scalar)
- node.raise "Expected scalar, not #{node.class}"
- end
-
- cookies = HTTP::Cookies.new
- node.value.split(";").each do |cookie|
- next if cookie.strip.empty?
- name, value = cookie.split("=", 2)
- cookies << HTTP::Cookie.new(name.strip, value.strip)
- end
-
- cookies
- end
- end
-end
-
def get_user(sid, headers, db, refresh = true)
if email = db.query_one?("SELECT email FROM session_ids WHERE id = $1", sid, as: String)
user = db.query_one("SELECT * FROM users WHERE email = $1", email, as: User)