mirror of
https://github.com/iv-org/invidious.git
synced 2025-08-16 01:28:29 +00:00
Merge 0989fe85d1
into 82e6f6e095
This commit is contained in:
commit
a032d3e374
@ -26,11 +26,13 @@ require "xml"
|
|||||||
require "yaml"
|
require "yaml"
|
||||||
require "compress/zip"
|
require "compress/zip"
|
||||||
require "protodec/utils"
|
require "protodec/utils"
|
||||||
|
|
||||||
require "./invidious/helpers/*"
|
require "./invidious/helpers/*"
|
||||||
require "./invidious/*"
|
require "./invidious/*"
|
||||||
require "./invidious/channels/*"
|
require "./invidious/channels/*"
|
||||||
require "./invidious/routes/**"
|
require "./invidious/routes/**"
|
||||||
require "./invidious/jobs/**"
|
require "./invidious/jobs/**"
|
||||||
|
require "./invidious/user/*"
|
||||||
|
|
||||||
CONFIG = Config.load
|
CONFIG = Config.load
|
||||||
HMAC_KEY = CONFIG.hmac_key || Random::Secure.hex(32)
|
HMAC_KEY = CONFIG.hmac_key || Random::Secure.hex(32)
|
||||||
@ -288,12 +290,14 @@ before_all do |env|
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
dark_mode = convert_theme(env.params.query["dark_mode"]?) || preferences.dark_mode.to_s
|
theme = env.params.query["dark_mode"]?
|
||||||
|
theme = Settings::Converters::Theme.from_s(theme) if theme
|
||||||
|
preferences.dark_mode = theme if theme
|
||||||
|
|
||||||
thin_mode = env.params.query["thin_mode"]? || preferences.thin_mode.to_s
|
thin_mode = env.params.query["thin_mode"]? || preferences.thin_mode.to_s
|
||||||
thin_mode = thin_mode == "true"
|
thin_mode = thin_mode == "true"
|
||||||
locale = env.params.query["hl"]? || preferences.locale
|
locale = env.params.query["hl"]? || preferences.locale
|
||||||
|
|
||||||
preferences.dark_mode = dark_mode
|
|
||||||
preferences.thin_mode = thin_mode
|
preferences.thin_mode = thin_mode
|
||||||
preferences.locale = locale
|
preferences.locale = locale
|
||||||
env.set "preferences", preferences
|
env.set "preferences", preferences
|
||||||
|
217
src/invidious/config.cr
Normal file
217
src/invidious/config.cr
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
#
|
||||||
|
# This file contains the server config data structures and
|
||||||
|
# all the associated validation/parsing routines.
|
||||||
|
#
|
||||||
|
|
||||||
|
struct DBConfig
|
||||||
|
include YAML::Serializable
|
||||||
|
|
||||||
|
property user : String
|
||||||
|
property password : String
|
||||||
|
|
||||||
|
property host : String
|
||||||
|
property port : Int32
|
||||||
|
|
||||||
|
property dbname : String
|
||||||
|
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: Config::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 : Preferences = Preferences.new
|
||||||
|
|
||||||
|
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'
|
||||||
|
|
||||||
|
@[YAML::Field(converter: Config::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: Config::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
|
||||||
|
|
||||||
|
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 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 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
|
@ -22,193 +22,6 @@ struct Annotation
|
|||||||
property annotations : String
|
property annotations : String
|
||||||
end
|
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'
|
|
||||||
|
|
||||||
@[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)
|
def login_req(f_req)
|
||||||
data = {
|
data = {
|
||||||
# Unfortunately there's not much information available on `bgRequest`; part of Google's BotGuard
|
# Unfortunately there's not much information available on `bgRequest`; part of Google's BotGuard
|
||||||
|
@ -397,19 +397,6 @@ def parse_range(range)
|
|||||||
return 0_i64, nil
|
return 0_i64, nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def convert_theme(theme)
|
|
||||||
case theme
|
|
||||||
when "true"
|
|
||||||
"dark"
|
|
||||||
when "false"
|
|
||||||
"light"
|
|
||||||
when "", nil
|
|
||||||
nil
|
|
||||||
else
|
|
||||||
theme
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def fetch_random_instance
|
def fetch_random_instance
|
||||||
begin
|
begin
|
||||||
instance_api_client = HTTP::Client.new(URI.parse("https://api.invidious.io"))
|
instance_api_client = HTTP::Client.new(URI.parse("https://api.invidious.io"))
|
||||||
|
@ -5,24 +5,22 @@ module Invidious::Routes::Misc
|
|||||||
user = env.get? "user"
|
user = env.get? "user"
|
||||||
|
|
||||||
case preferences.default_home
|
case preferences.default_home
|
||||||
when "Popular"
|
when Settings::HomePages::Popular ; env.redirect "/feed/popular"
|
||||||
env.redirect "/feed/popular"
|
when Settings::HomePages::Trending; env.redirect "/feed/trending"
|
||||||
when "Trending"
|
when Settings::UserHomePages::Subscriptions
|
||||||
env.redirect "/feed/trending"
|
|
||||||
when "Subscriptions"
|
|
||||||
if user
|
if user
|
||||||
env.redirect "/feed/subscriptions"
|
env.redirect "/feed/subscriptions"
|
||||||
else
|
else
|
||||||
env.redirect "/feed/popular"
|
env.redirect "/feed/popular"
|
||||||
end
|
end
|
||||||
when "Playlists"
|
when Settings::UserHomePages::Playlists
|
||||||
if user
|
if user
|
||||||
env.redirect "/feed/playlists"
|
env.redirect "/feed/playlists"
|
||||||
else
|
else
|
||||||
env.redirect "/feed/popular"
|
env.redirect "/feed/popular"
|
||||||
end
|
end
|
||||||
else
|
else # Settings::HomePages::Search
|
||||||
templated "search_homepage", navbar_search: false
|
env.redirect "/search"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -48,8 +48,8 @@ module Invidious::Routes::PreferencesRoute
|
|||||||
speed = env.params.body["speed"]?.try &.as(String).to_f32?
|
speed = env.params.body["speed"]?.try &.as(String).to_f32?
|
||||||
speed ||= CONFIG.default_user_preferences.speed
|
speed ||= CONFIG.default_user_preferences.speed
|
||||||
|
|
||||||
player_style = env.params.body["player_style"]?.try &.as(String)
|
player_style = env.params.body["player_style"]?.try { |x| Settings::PlayerStyles.parse?(x) }
|
||||||
player_style ||= CONFIG.default_user_preferences.player_style
|
player_style = CONFIG.default_user_preferences.player_style if !player_style
|
||||||
|
|
||||||
quality = env.params.body["quality"]?.try &.as(String)
|
quality = env.params.body["quality"]?.try &.as(String)
|
||||||
quality ||= CONFIG.default_user_preferences.quality
|
quality ||= CONFIG.default_user_preferences.quality
|
||||||
@ -86,7 +86,8 @@ module Invidious::Routes::PreferencesRoute
|
|||||||
related_videos ||= "off"
|
related_videos ||= "off"
|
||||||
related_videos = related_videos == "on"
|
related_videos = related_videos == "on"
|
||||||
|
|
||||||
default_home = env.params.body["default_home"]?.try &.as(String) || CONFIG.default_user_preferences.default_home
|
default_home = env.params.body["default_home"]?.try { |x| Settings::HomePages.parse?(x) || Settings::UserHomePages.parse?(x) }
|
||||||
|
default_home = CONFIG.default_user_preferences.default_home.to_s if !default_home
|
||||||
|
|
||||||
feed_menu = [] of String
|
feed_menu = [] of String
|
||||||
4.times do |index|
|
4.times do |index|
|
||||||
@ -103,8 +104,8 @@ module Invidious::Routes::PreferencesRoute
|
|||||||
locale = env.params.body["locale"]?.try &.as(String)
|
locale = env.params.body["locale"]?.try &.as(String)
|
||||||
locale ||= CONFIG.default_user_preferences.locale
|
locale ||= CONFIG.default_user_preferences.locale
|
||||||
|
|
||||||
dark_mode = env.params.body["dark_mode"]?.try &.as(String)
|
theme = env.params.body["dark_mode"]?.try { |x| Settings::Themes.parse?(x) }
|
||||||
dark_mode ||= CONFIG.default_user_preferences.dark_mode
|
theme = CONFIG.default_user_preferences.dark_mode if !theme
|
||||||
|
|
||||||
thin_mode = env.params.body["thin_mode"]?.try &.as(String)
|
thin_mode = env.params.body["thin_mode"]?.try &.as(String)
|
||||||
thin_mode ||= "off"
|
thin_mode ||= "off"
|
||||||
@ -113,8 +114,8 @@ module Invidious::Routes::PreferencesRoute
|
|||||||
max_results = env.params.body["max_results"]?.try &.as(String).to_i?
|
max_results = env.params.body["max_results"]?.try &.as(String).to_i?
|
||||||
max_results ||= CONFIG.default_user_preferences.max_results
|
max_results ||= CONFIG.default_user_preferences.max_results
|
||||||
|
|
||||||
sort = env.params.body["sort"]?.try &.as(String)
|
sort = env.params.body["sort"]?.try { |x| Settings::SortOptions.parse?(x) }
|
||||||
sort ||= CONFIG.default_user_preferences.sort
|
sort = CONFIG.default_user_preferences.sort if !sort
|
||||||
|
|
||||||
latest_only = env.params.body["latest_only"]?.try &.as(String)
|
latest_only = env.params.body["latest_only"]?.try &.as(String)
|
||||||
latest_only ||= "off"
|
latest_only ||= "off"
|
||||||
@ -137,7 +138,7 @@ module Invidious::Routes::PreferencesRoute
|
|||||||
comments: comments,
|
comments: comments,
|
||||||
continue: continue,
|
continue: continue,
|
||||||
continue_autoplay: continue_autoplay,
|
continue_autoplay: continue_autoplay,
|
||||||
dark_mode: dark_mode,
|
dark_mode: theme,
|
||||||
latest_only: latest_only,
|
latest_only: latest_only,
|
||||||
listen: listen,
|
listen: listen,
|
||||||
local: local,
|
local: local,
|
||||||
@ -167,14 +168,13 @@ module Invidious::Routes::PreferencesRoute
|
|||||||
PG_DB.exec("UPDATE users SET preferences = $1 WHERE email = $2", preferences, user.email)
|
PG_DB.exec("UPDATE users SET preferences = $1 WHERE email = $2", preferences, user.email)
|
||||||
|
|
||||||
if CONFIG.admins.includes? user.email
|
if CONFIG.admins.includes? user.email
|
||||||
CONFIG.default_user_preferences.default_home = env.params.body["admin_default_home"]?.try &.as(String) || CONFIG.default_user_preferences.default_home
|
default_home = env.params.body["admin_default_home"]?.try { |x| Settings::HomePages.parse?(x) || Settings::UserHomePages.parse?(x) }
|
||||||
|
CONFIG.default_user_preferences.default_home = default_home if default_home
|
||||||
|
|
||||||
admin_feed_menu = [] of String
|
admin_feed_menu = [] of Settings::AnyHomePages
|
||||||
4.times do |index|
|
4.times do |index|
|
||||||
option = env.params.body["admin_feed_menu[#{index}]"]?.try &.as(String) || ""
|
option = env.params.body["admin_feed_menu[#{index}]"]?.try { |x| Settings::HomePages.parse?(x) || Settings::UserHomePages.parse?(x) }
|
||||||
if !option.empty?
|
admin_feed_menu << option if option
|
||||||
admin_feed_menu << option
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
CONFIG.default_user_preferences.feed_menu = admin_feed_menu
|
CONFIG.default_user_preferences.feed_menu = admin_feed_menu
|
||||||
|
|
||||||
@ -229,28 +229,15 @@ module Invidious::Routes::PreferencesRoute
|
|||||||
|
|
||||||
if user = env.get? "user"
|
if user = env.get? "user"
|
||||||
user = user.as(User)
|
user = user.as(User)
|
||||||
|
|
||||||
preferences = user.preferences
|
preferences = user.preferences
|
||||||
|
preferences.toggle_theme
|
||||||
case preferences.dark_mode
|
|
||||||
when "dark"
|
|
||||||
preferences.dark_mode = "light"
|
|
||||||
else
|
|
||||||
preferences.dark_mode = "dark"
|
|
||||||
end
|
|
||||||
|
|
||||||
preferences = preferences.to_json
|
preferences = preferences.to_json
|
||||||
|
|
||||||
PG_DB.exec("UPDATE users SET preferences = $1 WHERE email = $2", preferences, user.email)
|
PG_DB.exec("UPDATE users SET preferences = $1 WHERE email = $2", preferences, user.email)
|
||||||
else
|
else
|
||||||
preferences = env.get("preferences").as(Preferences)
|
preferences = env.get("preferences").as(Preferences)
|
||||||
|
preferences.toggle_theme
|
||||||
case preferences.dark_mode
|
|
||||||
when "dark"
|
|
||||||
preferences.dark_mode = "light"
|
|
||||||
else
|
|
||||||
preferences.dark_mode = "dark"
|
|
||||||
end
|
|
||||||
|
|
||||||
preferences = preferences.to_json
|
preferences = preferences.to_json
|
||||||
|
|
||||||
if Kemal.config.ssl || CONFIG.https_only
|
if Kemal.config.ssl || CONFIG.https_only
|
||||||
|
757
src/invidious/user/preferences.cr
Normal file
757
src/invidious/user/preferences.cr
Normal file
@ -0,0 +1,757 @@
|
|||||||
|
#
|
||||||
|
# This file contains the user preferences data structures and
|
||||||
|
# all the associated validation/parsing routines.
|
||||||
|
#
|
||||||
|
|
||||||
|
require "json"
|
||||||
|
require "yaml"
|
||||||
|
require "html"
|
||||||
|
require "uri/params"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Enumerates types and constants
|
||||||
|
#
|
||||||
|
|
||||||
|
module Settings
|
||||||
|
ALLOWED_SPEED_VALUES = {
|
||||||
|
2.00_f32, # Double speed
|
||||||
|
1.75_f32,
|
||||||
|
1.50_f32,
|
||||||
|
1.25_f32,
|
||||||
|
1.00_f32, # Normal
|
||||||
|
0.75_f32,
|
||||||
|
0.50_f32, # Half speed
|
||||||
|
0.25_f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Themes
|
||||||
|
Auto # I.e use the system's settings with media queries
|
||||||
|
Light
|
||||||
|
Dark
|
||||||
|
end
|
||||||
|
|
||||||
|
enum PlayerStyles
|
||||||
|
Invidious
|
||||||
|
Youtube
|
||||||
|
end
|
||||||
|
|
||||||
|
# General
|
||||||
|
enum HomePages
|
||||||
|
Search
|
||||||
|
Popular
|
||||||
|
Trending
|
||||||
|
end
|
||||||
|
|
||||||
|
# Authenticated
|
||||||
|
enum UserHomePages
|
||||||
|
Subscriptions
|
||||||
|
Playlists
|
||||||
|
end
|
||||||
|
|
||||||
|
alias AnyHomePages = HomePages | UserHomePages
|
||||||
|
|
||||||
|
enum SortOptions
|
||||||
|
Alphabetically
|
||||||
|
Alphabetically_Reverse
|
||||||
|
Channel_Name
|
||||||
|
Channel_Name_Reverse
|
||||||
|
Publication_Date
|
||||||
|
Publication_Date_Reverse
|
||||||
|
end
|
||||||
|
|
||||||
|
# Unused for now, reuires merging
|
||||||
|
enum VideoQualities
|
||||||
|
# Normal
|
||||||
|
HD720
|
||||||
|
Medium
|
||||||
|
Small
|
||||||
|
# Dash
|
||||||
|
DASH_Auto
|
||||||
|
DASH_Best
|
||||||
|
DASH_4320p
|
||||||
|
DASH_2160p
|
||||||
|
DASH_1440p
|
||||||
|
DASH_1080p
|
||||||
|
DASH_720p
|
||||||
|
DASH_480p
|
||||||
|
DASH_360p
|
||||||
|
DASH_240p
|
||||||
|
DASH_144p
|
||||||
|
DASH_Worst
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
#
|
||||||
|
# Data structure that stores a user's preferences.
|
||||||
|
#
|
||||||
|
|
||||||
|
class Preferences
|
||||||
|
# Annotation for URL parameters metadata storage
|
||||||
|
# All the properties below can have one, or more associated URL parameter(s):
|
||||||
|
# - If no "short" nor "long" parameter are specified, the URL parameter
|
||||||
|
# will be the property name stringified.
|
||||||
|
# - Otherwise, a short and a long parameter must be specified
|
||||||
|
# - An optional "compat" parameter can be provided.
|
||||||
|
annotation AssociatedURLParams; end
|
||||||
|
|
||||||
|
include JSON::Serializable
|
||||||
|
include YAML::Serializable
|
||||||
|
|
||||||
|
property annotations : Bool = false
|
||||||
|
property annotations_subscribed : Bool = false
|
||||||
|
|
||||||
|
@[AssociatedURLParams(short: "ap", long: "autoplay")]
|
||||||
|
property autoplay : Bool = false
|
||||||
|
|
||||||
|
@[AssociatedURLParams(short: "autoredir", long: "auto_redirect")]
|
||||||
|
property automatic_instance_redirect : Bool = false
|
||||||
|
|
||||||
|
@[JSON::Field(converter: Preferences::StringToArray)]
|
||||||
|
@[YAML::Field(converter: Preferences::StringToArray)]
|
||||||
|
@[AssociatedURLParams(short: "cap", long: "captions")]
|
||||||
|
property captions : Array(String) = ["", "", ""]
|
||||||
|
|
||||||
|
@[JSON::Field(converter: Preferences::StringToArray)]
|
||||||
|
@[YAML::Field(converter: Preferences::StringToArray)]
|
||||||
|
@[AssociatedURLParams(short: "coms", long: "comments")]
|
||||||
|
property comments : Array(String) = ["youtube", ""]
|
||||||
|
|
||||||
|
property continue : Bool = false
|
||||||
|
property continue_autoplay : Bool = true
|
||||||
|
|
||||||
|
@[JSON::Field(converter: Settings::Converters::Generic(Settings::Themes))]
|
||||||
|
@[YAML::Field(converter: Settings::Converters::Generic(Settings::Themes))]
|
||||||
|
@[AssociatedURLParams(long: "theme", compat: "dark_mode")]
|
||||||
|
property dark_mode : Settings::Themes = Settings::Themes::Dark
|
||||||
|
|
||||||
|
property latest_only : Bool = false
|
||||||
|
property listen : Bool = false
|
||||||
|
property local : Bool = false
|
||||||
|
|
||||||
|
@[AssociatedURLParams(short: "vr", long: "vr_mode")]
|
||||||
|
property vr_mode : Bool = true
|
||||||
|
|
||||||
|
@[AssociatedURLParams(short: "nick", long: "show_nick")]
|
||||||
|
property show_nick : Bool = true
|
||||||
|
|
||||||
|
@[JSON::Field(converter: Preferences::ProcessString)]
|
||||||
|
@[AssociatedURLParams(short: "hl", long: "locale")]
|
||||||
|
property locale : String = "en-US"
|
||||||
|
|
||||||
|
@[JSON::Field(converter: Preferences::ClampInt)]
|
||||||
|
property max_results : Int32 = 40
|
||||||
|
|
||||||
|
@[AssociatedURLParams(short: "notifs_only", long: "notifications_only")]
|
||||||
|
property notifications_only : Bool = false
|
||||||
|
|
||||||
|
@[JSON::Field(converter: Settings::Converters::Generic(Settings::PlayerStyles))]
|
||||||
|
@[YAML::Field(converter: Settings::Converters::Generic(Settings::PlayerStyles))]
|
||||||
|
@[AssociatedURLParams(short: "ps", long: "player_style")]
|
||||||
|
property player_style : Settings::PlayerStyles = Settings::PlayerStyles::Invidious
|
||||||
|
|
||||||
|
@[JSON::Field(converter: Preferences::ProcessString)]
|
||||||
|
property quality : String = "hd720"
|
||||||
|
@[JSON::Field(converter: Preferences::ProcessString)]
|
||||||
|
property quality_dash : String = "auto"
|
||||||
|
|
||||||
|
@[JSON::Field(converter: Settings::Converters::Generic(Settings::AnyHomePages))]
|
||||||
|
@[YAML::Field(converter: Settings::Converters::Generic(Settings::AnyHomePages))]
|
||||||
|
@[AssociatedURLParams(short: "home", long: "default_home")]
|
||||||
|
property default_home : Settings::AnyHomePages = Settings::HomePages::Popular
|
||||||
|
|
||||||
|
@[JSON::Field(converter: Settings::Converters::Generic(Array(Settings::AnyHomePages)))]
|
||||||
|
@[YAML::Field(converter: Settings::Converters::Generic(Array(Settings::AnyHomePages)))]
|
||||||
|
@[AssociatedURLParams(short: "menu", long: "feed_menu")]
|
||||||
|
property feed_menu : Array(Settings::AnyHomePages) = [
|
||||||
|
Settings::HomePages::Popular,
|
||||||
|
Settings::HomePages::Trending,
|
||||||
|
Settings::UserHomePages::Subscriptions,
|
||||||
|
Settings::UserHomePages::Playlists,
|
||||||
|
]
|
||||||
|
|
||||||
|
property related_videos : Bool = true
|
||||||
|
|
||||||
|
@[JSON::Field(converter: Settings::Converters::Generic(Settings::SortOptions))]
|
||||||
|
@[YAML::Field(converter: Settings::Converters::Generic(Settings::SortOptions))]
|
||||||
|
property sort : Settings::SortOptions = Settings::SortOptions::Publication_Date
|
||||||
|
|
||||||
|
property speed : Float32 = 1.0_f32
|
||||||
|
|
||||||
|
property thin_mode : Bool = false
|
||||||
|
property unseen_only : Bool = false
|
||||||
|
|
||||||
|
@[AssociatedURLParams(short: "loop", long: "video_loop")]
|
||||||
|
property video_loop : Bool = false
|
||||||
|
|
||||||
|
@[AssociatedURLParams(short: "xd", long: "extend_desc")]
|
||||||
|
property extend_desc : Bool = false
|
||||||
|
|
||||||
|
@[AssociatedURLParams(short: "vol", long: "volume")]
|
||||||
|
property volume : Int32 = 100
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
end
|
||||||
|
|
||||||
|
# Duplicate (make a perfect copy of) the current object
|
||||||
|
def dup
|
||||||
|
other = Preferences.new
|
||||||
|
|
||||||
|
{% for ivar in @type.instance_vars %}
|
||||||
|
other.{{ ivar.id }} = {{ ivar.id }}
|
||||||
|
{% end %}
|
||||||
|
|
||||||
|
return other
|
||||||
|
end
|
||||||
|
|
||||||
|
# :nodoc:
|
||||||
|
def text_dump(include_defaults = false)
|
||||||
|
{% for ivar in @type.instance_vars %}
|
||||||
|
name = {{ ivar.id.stringify }}
|
||||||
|
value = {{ ivar.id }}
|
||||||
|
default = {{ ivar.default_value }}
|
||||||
|
|
||||||
|
puts "#{name} = #{value}" if (include_defaults || value != default)
|
||||||
|
{% end %}
|
||||||
|
end
|
||||||
|
|
||||||
|
# :nodoc:
|
||||||
|
def text_dump(other : Preferences)
|
||||||
|
# Macro that dumps, as text, all the contents of that struct
|
||||||
|
{% for ivar in @type.instance_vars %}
|
||||||
|
name = {{ ivar.id.stringify }}
|
||||||
|
val1 = self.{{ ivar.id }}
|
||||||
|
val2 = other.{{ ivar.id }}
|
||||||
|
|
||||||
|
puts "#{name} = #{val1}" if val1 != val2
|
||||||
|
{% end %}
|
||||||
|
end
|
||||||
|
|
||||||
|
def toggle_theme
|
||||||
|
old = @dark_mode
|
||||||
|
@dark_mode = (old.dark?) ? Settings::Themes::Light : Settings::Themes::Dark
|
||||||
|
end
|
||||||
|
|
||||||
|
# From/To URI parameters
|
||||||
|
|
||||||
|
def self.from_uri(uri : String) : Preferences
|
||||||
|
return from_uri(URI::Params.parse(uri))
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.from_uri(uri : URI::Params) : Preferences
|
||||||
|
prefs = Preferences.new
|
||||||
|
|
||||||
|
{% begin %}
|
||||||
|
|
||||||
|
{% for ivar in @type.instance_vars %}
|
||||||
|
{%
|
||||||
|
uri_ann = ivar.annotation(::Preferences::AssociatedURLParams)
|
||||||
|
default = ivar.default_value
|
||||||
|
max_idx = (ivar.type < Array) ? default.size - 1 : nil
|
||||||
|
|
||||||
|
has_long = (uri_ann && uri_ann[:long])
|
||||||
|
has_short = (uri_ann && uri_ann[:short])
|
||||||
|
has_compat = (uri_ann && uri_ann[:compat])
|
||||||
|
|
||||||
|
param_long = has_long ? uri_ann[:long] : ivar.id.stringify
|
||||||
|
param_short = has_short ? uri_ann[:short] : ivar.id.stringify
|
||||||
|
%}
|
||||||
|
|
||||||
|
# Arrays in HTTP forms are not standardized, so we have to
|
||||||
|
# support those manually here
|
||||||
|
{% if max_idx %}
|
||||||
|
temp = [] of String | Nil
|
||||||
|
|
||||||
|
(0..{{max_idx}}).each do |i|
|
||||||
|
temp_val = uri[{{param_long}}+"[#{i}]"]? || uri[{{param_short}}+"[#{i}]"]?
|
||||||
|
|
||||||
|
# If "compat" is available and nothing was found above, also check that
|
||||||
|
{% if has_compat %}
|
||||||
|
temp_val = uri[{{uri_ann[:compat]}}+"[#{i}]"]? if !temp_val
|
||||||
|
{% end %}
|
||||||
|
|
||||||
|
temp << temp_val
|
||||||
|
end
|
||||||
|
{% else %}
|
||||||
|
temp = uri[{{param_long}}]? || uri[{{param_short}}]?
|
||||||
|
|
||||||
|
# If "compat" is available and nothing was found above, also check that
|
||||||
|
{% if has_compat %}
|
||||||
|
temp = uri[{{uri_ann[:compat]}}]? if !temp
|
||||||
|
{% end %}
|
||||||
|
{% end %}
|
||||||
|
|
||||||
|
# Then, use the right converter and assign the variable
|
||||||
|
# We use a bunch of if/elsif/else statements because case/when can't
|
||||||
|
# be used in macros.
|
||||||
|
{% if ivar.type == Settings::Themes %}
|
||||||
|
val = Settings::Converters::Theme.from_s(temp)
|
||||||
|
{% elsif ivar.type == Enum %}
|
||||||
|
val = {{ivar.type}}.parse?(temp)
|
||||||
|
{% elsif ivar.type == Array(Enum) %}
|
||||||
|
val = [] of {{ivar.type}}
|
||||||
|
temp.compact.each { |item| val << {{ivar.type}}.parse?(item) }
|
||||||
|
{% elsif ivar.type == Bool %}
|
||||||
|
val = Settings::Converters::Boolean.from_s(temp)
|
||||||
|
{% elsif ivar.type == String || ivar.type == Array(String) %}
|
||||||
|
val = temp
|
||||||
|
{% elsif ivar.type == Float32 %}
|
||||||
|
val = temp.try &.as(String).to_f32?
|
||||||
|
{% else %}
|
||||||
|
val = nil
|
||||||
|
{% end %}
|
||||||
|
|
||||||
|
{% if max_idx %}
|
||||||
|
prefs.{{ivar.id}} = val.compact if val
|
||||||
|
{% else %}
|
||||||
|
prefs.{{ivar.id}} = val if val
|
||||||
|
{% end %}
|
||||||
|
{% end %}
|
||||||
|
|
||||||
|
{% end %}
|
||||||
|
|
||||||
|
return prefs
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_uri(*, use_short = false, include_defaults = false) : URI::Params
|
||||||
|
params_raw = {} of String => Array(String)
|
||||||
|
# Hash(String, Array(String)))
|
||||||
|
|
||||||
|
{% for ivar in @type.instance_vars %}
|
||||||
|
{%
|
||||||
|
uri_ann = ivar.annotation(::Preferences::AssociatedURLParams)
|
||||||
|
|
||||||
|
has_long = (uri_ann && uri_ann[:long])
|
||||||
|
has_short = (uri_ann && uri_ann[:short])
|
||||||
|
|
||||||
|
param_long = has_long ? uri_ann[:long] : ivar.id.stringify
|
||||||
|
param_short = has_short ? uri_ann[:short] : param_long
|
||||||
|
%}
|
||||||
|
|
||||||
|
# If 'include_defaults' is false, check that the current
|
||||||
|
# value differs from the default
|
||||||
|
if include_defaults || @{{ivar.id}} != {{ivar.default_value}}
|
||||||
|
param_name = use_short ? {{param_short}} : {{param_long}}
|
||||||
|
|
||||||
|
{% if ivar.type == Bool %}
|
||||||
|
# Boolean is kinda special. We want to use 'true/false' for the
|
||||||
|
# long form and '0/1' for the short form
|
||||||
|
temp = Settings::Converters::Boolean.to_s(@{{ivar.id}}, numeric: use_short)
|
||||||
|
params_raw[param_name] = [temp]
|
||||||
|
{% elsif ivar.type < Array %}
|
||||||
|
# Arrays. Use `#to_s` if not already a string.
|
||||||
|
@{{ivar.id}}.each_with_index do |value, idx|
|
||||||
|
{% if ivar.type == Array(String) %}
|
||||||
|
params_raw[param_name + "[#{idx}]"] = [value]
|
||||||
|
{% else %}
|
||||||
|
params_raw[param_name + "[#{idx}]"] = [value.to_s]
|
||||||
|
{% end %}
|
||||||
|
end
|
||||||
|
{% else %}
|
||||||
|
# Same as aboce, but with scalars
|
||||||
|
{% if ivar.type == String %}
|
||||||
|
params_raw[param_name] = [@{{ivar.id}}]
|
||||||
|
{% else %}
|
||||||
|
params_raw[param_name] = [@{{ivar.id}}.to_s]
|
||||||
|
{% end %}
|
||||||
|
{% end %}
|
||||||
|
end
|
||||||
|
|
||||||
|
{% end %}
|
||||||
|
|
||||||
|
return URI::Params.new(params_raw)
|
||||||
|
end
|
||||||
|
|
||||||
|
# volume = env.params.body["volume"]?.try &.as(String).to_i?
|
||||||
|
# volume ||= CONFIG.default_user_preferences.volume
|
||||||
|
|
||||||
|
# comments = [] of String
|
||||||
|
# 2.times do |i|
|
||||||
|
# comments << (env.params.body["comments[#{i}]"]?.try &.as(String) || CONFIG.default_user_preferences.comments[i])
|
||||||
|
# end
|
||||||
|
|
||||||
|
# captions = [] of String
|
||||||
|
# 3.times do |i|
|
||||||
|
# captions << (env.params.body["captions[#{i}]"]?.try &.as(String) || CONFIG.default_user_preferences.captions[i])
|
||||||
|
# end
|
||||||
|
|
||||||
|
# default_home = env.params.body["default_home"]?.try { |x| Settings::HomePages.parse?(x) || Settings::UserHomePages.parse?(x) }
|
||||||
|
# default_home = CONFIG.default_user_preferences.default_home.to_s if !default_home
|
||||||
|
|
||||||
|
# feed_menu = [] of String
|
||||||
|
# 4.times do |index|
|
||||||
|
# option = env.params.body["feed_menu[#{index}]"]?.try &.as(String) || ""
|
||||||
|
# if !option.empty?
|
||||||
|
# feed_menu << option
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
|
||||||
|
# Old converters (TODO: remove or refactor them)
|
||||||
|
|
||||||
|
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 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
|
||||||
|
|
||||||
|
def to_tuple
|
||||||
|
{% begin %}
|
||||||
|
{
|
||||||
|
{{*@type.instance_vars.map { |var| "#{var.name}: #{var.name}".id }}}
|
||||||
|
}
|
||||||
|
{% end %}
|
||||||
|
end
|
||||||
|
end # class Data
|
||||||
|
|
||||||
|
#
|
||||||
|
# Datatype converters (also act as data validators)
|
||||||
|
#
|
||||||
|
|
||||||
|
module Settings::Converters
|
||||||
|
#
|
||||||
|
# Generic enum conversion
|
||||||
|
#
|
||||||
|
module Generic(T)
|
||||||
|
extend self
|
||||||
|
|
||||||
|
# From/To JSON
|
||||||
|
|
||||||
|
def to_json(value : T, json : JSON::Builder)
|
||||||
|
value.to_json json
|
||||||
|
end
|
||||||
|
|
||||||
|
def from_json(value : JSON::PullParser) : T?
|
||||||
|
begin
|
||||||
|
T.new value
|
||||||
|
rescue e : JSON::ParseException
|
||||||
|
# Be silent on invalid data and return nil
|
||||||
|
# This will fallback to the default value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# From/To YAML
|
||||||
|
|
||||||
|
def to_yaml(value : T, yaml : YAML::Nodes::Builder)
|
||||||
|
value.to_yaml yaml
|
||||||
|
end
|
||||||
|
|
||||||
|
def from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : T?
|
||||||
|
begin
|
||||||
|
T.new ctx, node
|
||||||
|
rescue e : YAML::ParseException
|
||||||
|
# Be silent on invalid data and return nil
|
||||||
|
# This will fallback to the default value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end # module Generic
|
||||||
|
|
||||||
|
#
|
||||||
|
# Themes enum conversion
|
||||||
|
#
|
||||||
|
module Theme
|
||||||
|
extend self
|
||||||
|
|
||||||
|
# From String (.to_s is native of Enum type)
|
||||||
|
|
||||||
|
def from_s(input : String?) : Themes?
|
||||||
|
return if !input
|
||||||
|
|
||||||
|
case input.downcase
|
||||||
|
when "auto" ; Themes::Auto
|
||||||
|
when "light"; Themes::Light
|
||||||
|
when "dark" ; Themes::Dark
|
||||||
|
# Compatibility with old 'dark_mode' values
|
||||||
|
when "false"; Themes::Light
|
||||||
|
when "true" ; Themes::Dark
|
||||||
|
else
|
||||||
|
# Nothing, use default from initialization
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# From/To JSON
|
||||||
|
|
||||||
|
def to_json(value : Themes, json : JSON::Builder)
|
||||||
|
json.string value.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
def from_json(value : JSON::PullParser) : Themes?
|
||||||
|
return self.from_s(value.read_string)
|
||||||
|
end
|
||||||
|
|
||||||
|
# From/To YAML
|
||||||
|
|
||||||
|
def to_yaml(value : Themes, yaml : YAML::Nodes::Builder)
|
||||||
|
yaml.scalar value.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
def from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : Themes?
|
||||||
|
unless node.is_a?(YAML::Nodes::Scalar)
|
||||||
|
node.raise "Expected scalar, not #{node.class}"
|
||||||
|
end
|
||||||
|
|
||||||
|
return self.from_s(node.value)
|
||||||
|
end
|
||||||
|
end # module theme
|
||||||
|
|
||||||
|
#
|
||||||
|
# String to Boolean conversion
|
||||||
|
#
|
||||||
|
module Boolean
|
||||||
|
extend self
|
||||||
|
|
||||||
|
def from_s(input : String?) : Bool?
|
||||||
|
return if !input
|
||||||
|
|
||||||
|
case input.downcase
|
||||||
|
when "0", "off", "false"; false
|
||||||
|
when "1", "on", "true" ; true
|
||||||
|
else
|
||||||
|
nil # invalid input, do nothing.
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s(input : Bool, numeric = false) : String
|
||||||
|
if numeric
|
||||||
|
return input ? "1" : "0"
|
||||||
|
else
|
||||||
|
return input ? "true" : "false"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end # module Settings::Converters
|
||||||
|
|
||||||
|
struct VideoPreferences
|
||||||
|
include JSON::Serializable
|
||||||
|
|
||||||
|
property annotations : Bool
|
||||||
|
property autoplay : Bool
|
||||||
|
property comments : Array(String)
|
||||||
|
property continue : Bool
|
||||||
|
property continue_autoplay : Bool
|
||||||
|
property controls : Bool
|
||||||
|
property listen : Bool
|
||||||
|
property local : Bool
|
||||||
|
property preferred_captions : Array(String)
|
||||||
|
property player_style : String
|
||||||
|
property quality : String
|
||||||
|
property quality_dash : String
|
||||||
|
property raw : Bool
|
||||||
|
property region : String?
|
||||||
|
property related_videos : Bool
|
||||||
|
property speed : Float32 | Float64
|
||||||
|
property video_end : Float64 | Int32
|
||||||
|
property video_loop : Bool
|
||||||
|
property extend_desc : Bool
|
||||||
|
property video_start : Float64 | Int32
|
||||||
|
property volume : Int32
|
||||||
|
property vr_mode : Bool
|
||||||
|
end
|
||||||
|
|
||||||
|
def process_video_params(query, preferences)
|
||||||
|
annotations = query["iv_load_policy"]?.try &.to_i?
|
||||||
|
autoplay = query["autoplay"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||||
|
comments = query["comments"]?.try &.split(",").map { |a| a.downcase }
|
||||||
|
continue = query["continue"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||||
|
continue_autoplay = query["continue_autoplay"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||||
|
listen = query["listen"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||||
|
local = query["local"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||||
|
player_style = query["player_style"]?
|
||||||
|
preferred_captions = query["subtitles"]?.try &.split(",").map { |a| a.downcase }
|
||||||
|
quality = query["quality"]?
|
||||||
|
quality_dash = query["quality_dash"]?
|
||||||
|
region = query["region"]?
|
||||||
|
related_videos = query["related_videos"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||||
|
speed = query["speed"]?.try &.rchop("x").to_f?
|
||||||
|
video_loop = query["loop"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||||
|
extend_desc = query["extend_desc"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||||
|
volume = query["volume"]?.try &.to_i?
|
||||||
|
vr_mode = query["vr_mode"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||||
|
|
||||||
|
if preferences
|
||||||
|
# region ||= preferences.region
|
||||||
|
annotations ||= preferences.annotations.to_unsafe
|
||||||
|
autoplay ||= preferences.autoplay.to_unsafe
|
||||||
|
comments ||= preferences.comments
|
||||||
|
continue ||= preferences.continue.to_unsafe
|
||||||
|
continue_autoplay ||= preferences.continue_autoplay.to_unsafe
|
||||||
|
listen ||= preferences.listen.to_unsafe
|
||||||
|
local ||= preferences.local.to_unsafe
|
||||||
|
player_style ||= preferences.player_style.to_s
|
||||||
|
preferred_captions ||= preferences.captions
|
||||||
|
quality ||= preferences.quality
|
||||||
|
quality_dash ||= preferences.quality_dash
|
||||||
|
related_videos ||= preferences.related_videos.to_unsafe
|
||||||
|
speed ||= preferences.speed
|
||||||
|
video_loop ||= preferences.video_loop.to_unsafe
|
||||||
|
extend_desc ||= preferences.extend_desc.to_unsafe
|
||||||
|
volume ||= preferences.volume
|
||||||
|
vr_mode ||= preferences.vr_mode.to_unsafe
|
||||||
|
end
|
||||||
|
|
||||||
|
annotations ||= CONFIG.default_user_preferences.annotations.to_unsafe
|
||||||
|
autoplay ||= CONFIG.default_user_preferences.autoplay.to_unsafe
|
||||||
|
comments ||= CONFIG.default_user_preferences.comments
|
||||||
|
continue ||= CONFIG.default_user_preferences.continue.to_unsafe
|
||||||
|
continue_autoplay ||= CONFIG.default_user_preferences.continue_autoplay.to_unsafe
|
||||||
|
listen ||= CONFIG.default_user_preferences.listen.to_unsafe
|
||||||
|
local ||= CONFIG.default_user_preferences.local.to_unsafe
|
||||||
|
player_style ||= CONFIG.default_user_preferences.player_style.to_s
|
||||||
|
preferred_captions ||= CONFIG.default_user_preferences.captions
|
||||||
|
quality ||= CONFIG.default_user_preferences.quality
|
||||||
|
quality_dash ||= CONFIG.default_user_preferences.quality_dash
|
||||||
|
related_videos ||= CONFIG.default_user_preferences.related_videos.to_unsafe
|
||||||
|
speed ||= CONFIG.default_user_preferences.speed
|
||||||
|
video_loop ||= CONFIG.default_user_preferences.video_loop.to_unsafe
|
||||||
|
extend_desc ||= CONFIG.default_user_preferences.extend_desc.to_unsafe
|
||||||
|
volume ||= CONFIG.default_user_preferences.volume
|
||||||
|
vr_mode ||= CONFIG.default_user_preferences.vr_mode.to_unsafe
|
||||||
|
|
||||||
|
annotations = annotations == 1
|
||||||
|
autoplay = autoplay == 1
|
||||||
|
continue = continue == 1
|
||||||
|
continue_autoplay = continue_autoplay == 1
|
||||||
|
listen = listen == 1
|
||||||
|
local = local == 1
|
||||||
|
related_videos = related_videos == 1
|
||||||
|
video_loop = video_loop == 1
|
||||||
|
extend_desc = extend_desc == 1
|
||||||
|
vr_mode = vr_mode == 1
|
||||||
|
|
||||||
|
if CONFIG.disabled?("dash") && quality == "dash"
|
||||||
|
quality = "high"
|
||||||
|
end
|
||||||
|
|
||||||
|
if CONFIG.disabled?("local") && local
|
||||||
|
local = false
|
||||||
|
end
|
||||||
|
|
||||||
|
if start = query["t"]? || query["time_continue"]? || query["start"]?
|
||||||
|
video_start = decode_time(start)
|
||||||
|
end
|
||||||
|
video_start ||= 0
|
||||||
|
|
||||||
|
if query["end"]?
|
||||||
|
video_end = decode_time(query["end"])
|
||||||
|
end
|
||||||
|
video_end ||= -1
|
||||||
|
|
||||||
|
raw = query["raw"]?.try &.to_i?
|
||||||
|
raw ||= 0
|
||||||
|
raw = raw == 1
|
||||||
|
|
||||||
|
controls = query["controls"]?.try &.to_i?
|
||||||
|
controls ||= 1
|
||||||
|
controls = controls >= 1
|
||||||
|
|
||||||
|
params = VideoPreferences.new({
|
||||||
|
annotations: annotations,
|
||||||
|
autoplay: autoplay,
|
||||||
|
comments: comments,
|
||||||
|
continue: continue,
|
||||||
|
continue_autoplay: continue_autoplay,
|
||||||
|
controls: controls,
|
||||||
|
listen: listen,
|
||||||
|
local: local,
|
||||||
|
player_style: player_style,
|
||||||
|
preferred_captions: preferred_captions,
|
||||||
|
quality: quality,
|
||||||
|
quality_dash: quality_dash,
|
||||||
|
raw: raw,
|
||||||
|
region: region,
|
||||||
|
related_videos: related_videos,
|
||||||
|
speed: speed,
|
||||||
|
video_end: video_end,
|
||||||
|
video_loop: video_loop,
|
||||||
|
extend_desc: extend_desc,
|
||||||
|
video_start: video_start,
|
||||||
|
volume: volume,
|
||||||
|
vr_mode: vr_mode,
|
||||||
|
})
|
||||||
|
|
||||||
|
return params
|
||||||
|
end
|
@ -29,264 +29,6 @@ struct User
|
|||||||
end
|
end
|
||||||
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)
|
def get_user(sid, headers, db, refresh = true)
|
||||||
if email = db.query_one?("SELECT email FROM session_ids WHERE id = $1", sid, as: String)
|
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)
|
user = db.query_one("SELECT * FROM users WHERE email = $1", email, as: User)
|
||||||
@ -509,15 +251,12 @@ def get_subscription_feed(db, user, max_results = 40, page = 1)
|
|||||||
notifications.sort_by! { |video| video.published }.reverse!
|
notifications.sort_by! { |video| video.published }.reverse!
|
||||||
|
|
||||||
case user.preferences.sort
|
case user.preferences.sort
|
||||||
when "alphabetically"
|
when .alphabetically? ; notifications.sort_by! { |video| video.title }
|
||||||
notifications.sort_by! { |video| video.title }
|
when .alphabetically_reverse?; notifications.sort_by! { |video| video.title }.reverse!
|
||||||
when "alphabetically - reverse"
|
when .channel_name? ; notifications.sort_by! { |video| video.author }
|
||||||
notifications.sort_by! { |video| video.title }.reverse!
|
when .channel_name_reverse? ; notifications.sort_by! { |video| video.author }.reverse!
|
||||||
when "channel name"
|
else
|
||||||
notifications.sort_by! { |video| video.author }
|
nil # Ignore
|
||||||
when "channel name - reverse"
|
|
||||||
notifications.sort_by! { |video| video.author }.reverse!
|
|
||||||
else nil # Ignore
|
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
if user.preferences.latest_only
|
if user.preferences.latest_only
|
||||||
@ -556,16 +295,13 @@ def get_subscription_feed(db, user, max_results = 40, page = 1)
|
|||||||
end
|
end
|
||||||
|
|
||||||
case user.preferences.sort
|
case user.preferences.sort
|
||||||
when "published - reverse"
|
when .alphabetically? ; videos.sort_by! { |video| video.title }
|
||||||
videos.sort_by! { |video| video.published }
|
when .alphabetically_reverse?; videos.sort_by! { |video| video.title }.reverse!
|
||||||
when "alphabetically"
|
when .channel_name? ; videos.sort_by! { |video| video.author }
|
||||||
videos.sort_by! { |video| video.title }
|
when .channel_name_reverse? ; videos.sort_by! { |video| video.author }.reverse!
|
||||||
when "alphabetically - reverse"
|
when .publication_date? ; videos.sort_by! { |video| video.published }
|
||||||
videos.sort_by! { |video| video.title }.reverse!
|
# when .publication_date_reverse?; videos.sort_by! { |video| video.published }.reverse!
|
||||||
when "channel name"
|
# "Date reverse" wasn't here originally (why????)
|
||||||
videos.sort_by! { |video| video.author }
|
|
||||||
when "channel name - reverse"
|
|
||||||
videos.sort_by! { |video| video.author }.reverse!
|
|
||||||
else nil # Ignore
|
else nil # Ignore
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -221,33 +221,6 @@ VIDEO_FORMATS = {
|
|||||||
"397" => {"ext" => "mp4", "height" => 480, "vcodec" => "av01.0.05M.08"},
|
"397" => {"ext" => "mp4", "height" => 480, "vcodec" => "av01.0.05M.08"},
|
||||||
}
|
}
|
||||||
|
|
||||||
struct VideoPreferences
|
|
||||||
include JSON::Serializable
|
|
||||||
|
|
||||||
property annotations : Bool
|
|
||||||
property autoplay : Bool
|
|
||||||
property comments : Array(String)
|
|
||||||
property continue : Bool
|
|
||||||
property continue_autoplay : Bool
|
|
||||||
property controls : Bool
|
|
||||||
property listen : Bool
|
|
||||||
property local : Bool
|
|
||||||
property preferred_captions : Array(String)
|
|
||||||
property player_style : String
|
|
||||||
property quality : String
|
|
||||||
property quality_dash : String
|
|
||||||
property raw : Bool
|
|
||||||
property region : String?
|
|
||||||
property related_videos : Bool
|
|
||||||
property speed : Float32 | Float64
|
|
||||||
property video_end : Float64 | Int32
|
|
||||||
property video_loop : Bool
|
|
||||||
property extend_desc : Bool
|
|
||||||
property video_start : Float64 | Int32
|
|
||||||
property volume : Int32
|
|
||||||
property vr_mode : Bool
|
|
||||||
end
|
|
||||||
|
|
||||||
struct Video
|
struct Video
|
||||||
include DB::Serializable
|
include DB::Serializable
|
||||||
|
|
||||||
@ -1020,130 +993,6 @@ def process_continuation(db, query, plid, id)
|
|||||||
continuation
|
continuation
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_video_params(query, preferences)
|
|
||||||
annotations = query["iv_load_policy"]?.try &.to_i?
|
|
||||||
autoplay = query["autoplay"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
|
||||||
comments = query["comments"]?.try &.split(",").map { |a| a.downcase }
|
|
||||||
continue = query["continue"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
|
||||||
continue_autoplay = query["continue_autoplay"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
|
||||||
listen = query["listen"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
|
||||||
local = query["local"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
|
||||||
player_style = query["player_style"]?
|
|
||||||
preferred_captions = query["subtitles"]?.try &.split(",").map { |a| a.downcase }
|
|
||||||
quality = query["quality"]?
|
|
||||||
quality_dash = query["quality_dash"]?
|
|
||||||
region = query["region"]?
|
|
||||||
related_videos = query["related_videos"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
|
||||||
speed = query["speed"]?.try &.rchop("x").to_f?
|
|
||||||
video_loop = query["loop"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
|
||||||
extend_desc = query["extend_desc"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
|
||||||
volume = query["volume"]?.try &.to_i?
|
|
||||||
vr_mode = query["vr_mode"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
|
||||||
|
|
||||||
if preferences
|
|
||||||
# region ||= preferences.region
|
|
||||||
annotations ||= preferences.annotations.to_unsafe
|
|
||||||
autoplay ||= preferences.autoplay.to_unsafe
|
|
||||||
comments ||= preferences.comments
|
|
||||||
continue ||= preferences.continue.to_unsafe
|
|
||||||
continue_autoplay ||= preferences.continue_autoplay.to_unsafe
|
|
||||||
listen ||= preferences.listen.to_unsafe
|
|
||||||
local ||= preferences.local.to_unsafe
|
|
||||||
player_style ||= preferences.player_style
|
|
||||||
preferred_captions ||= preferences.captions
|
|
||||||
quality ||= preferences.quality
|
|
||||||
quality_dash ||= preferences.quality_dash
|
|
||||||
related_videos ||= preferences.related_videos.to_unsafe
|
|
||||||
speed ||= preferences.speed
|
|
||||||
video_loop ||= preferences.video_loop.to_unsafe
|
|
||||||
extend_desc ||= preferences.extend_desc.to_unsafe
|
|
||||||
volume ||= preferences.volume
|
|
||||||
vr_mode ||= preferences.vr_mode.to_unsafe
|
|
||||||
end
|
|
||||||
|
|
||||||
annotations ||= CONFIG.default_user_preferences.annotations.to_unsafe
|
|
||||||
autoplay ||= CONFIG.default_user_preferences.autoplay.to_unsafe
|
|
||||||
comments ||= CONFIG.default_user_preferences.comments
|
|
||||||
continue ||= CONFIG.default_user_preferences.continue.to_unsafe
|
|
||||||
continue_autoplay ||= CONFIG.default_user_preferences.continue_autoplay.to_unsafe
|
|
||||||
listen ||= CONFIG.default_user_preferences.listen.to_unsafe
|
|
||||||
local ||= CONFIG.default_user_preferences.local.to_unsafe
|
|
||||||
player_style ||= CONFIG.default_user_preferences.player_style
|
|
||||||
preferred_captions ||= CONFIG.default_user_preferences.captions
|
|
||||||
quality ||= CONFIG.default_user_preferences.quality
|
|
||||||
quality_dash ||= CONFIG.default_user_preferences.quality_dash
|
|
||||||
related_videos ||= CONFIG.default_user_preferences.related_videos.to_unsafe
|
|
||||||
speed ||= CONFIG.default_user_preferences.speed
|
|
||||||
video_loop ||= CONFIG.default_user_preferences.video_loop.to_unsafe
|
|
||||||
extend_desc ||= CONFIG.default_user_preferences.extend_desc.to_unsafe
|
|
||||||
volume ||= CONFIG.default_user_preferences.volume
|
|
||||||
vr_mode ||= CONFIG.default_user_preferences.vr_mode.to_unsafe
|
|
||||||
|
|
||||||
annotations = annotations == 1
|
|
||||||
autoplay = autoplay == 1
|
|
||||||
continue = continue == 1
|
|
||||||
continue_autoplay = continue_autoplay == 1
|
|
||||||
listen = listen == 1
|
|
||||||
local = local == 1
|
|
||||||
related_videos = related_videos == 1
|
|
||||||
video_loop = video_loop == 1
|
|
||||||
extend_desc = extend_desc == 1
|
|
||||||
vr_mode = vr_mode == 1
|
|
||||||
|
|
||||||
if CONFIG.disabled?("dash") && quality == "dash"
|
|
||||||
quality = "high"
|
|
||||||
end
|
|
||||||
|
|
||||||
if CONFIG.disabled?("local") && local
|
|
||||||
local = false
|
|
||||||
end
|
|
||||||
|
|
||||||
if start = query["t"]? || query["time_continue"]? || query["start"]?
|
|
||||||
video_start = decode_time(start)
|
|
||||||
end
|
|
||||||
video_start ||= 0
|
|
||||||
|
|
||||||
if query["end"]?
|
|
||||||
video_end = decode_time(query["end"])
|
|
||||||
end
|
|
||||||
video_end ||= -1
|
|
||||||
|
|
||||||
raw = query["raw"]?.try &.to_i?
|
|
||||||
raw ||= 0
|
|
||||||
raw = raw == 1
|
|
||||||
|
|
||||||
controls = query["controls"]?.try &.to_i?
|
|
||||||
controls ||= 1
|
|
||||||
controls = controls >= 1
|
|
||||||
|
|
||||||
params = VideoPreferences.new({
|
|
||||||
annotations: annotations,
|
|
||||||
autoplay: autoplay,
|
|
||||||
comments: comments,
|
|
||||||
continue: continue,
|
|
||||||
continue_autoplay: continue_autoplay,
|
|
||||||
controls: controls,
|
|
||||||
listen: listen,
|
|
||||||
local: local,
|
|
||||||
player_style: player_style,
|
|
||||||
preferred_captions: preferred_captions,
|
|
||||||
quality: quality,
|
|
||||||
quality_dash: quality_dash,
|
|
||||||
raw: raw,
|
|
||||||
region: region,
|
|
||||||
related_videos: related_videos,
|
|
||||||
speed: speed,
|
|
||||||
video_end: video_end,
|
|
||||||
video_loop: video_loop,
|
|
||||||
extend_desc: extend_desc,
|
|
||||||
video_start: video_start,
|
|
||||||
volume: volume,
|
|
||||||
vr_mode: vr_mode,
|
|
||||||
})
|
|
||||||
|
|
||||||
return params
|
|
||||||
end
|
|
||||||
|
|
||||||
def build_thumbnails(id)
|
def build_thumbnails(id)
|
||||||
return {
|
return {
|
||||||
{host: HOST_URL, height: 720, width: 1280, name: "maxres", url: "maxres"},
|
{host: HOST_URL, height: 720, width: 1280, name: "maxres", url: "maxres"},
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
<div class="feed-menu">
|
<div class="feed-menu">
|
||||||
<% feed_menu = env.get("preferences").as(Preferences).feed_menu.dup %>
|
<% feed_menu = env.get("preferences").as(Preferences).feed_menu.dup %>
|
||||||
<% if !env.get?("user") %>
|
<% if !env.get?("user") %>
|
||||||
<% feed_menu.reject! {|item| {"Subscriptions", "Playlists"}.includes? item} %>
|
<% feed_menu.reject! {|item| Settings::UserHomePages.names.includes? item} %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% feed_menu.each do |feed| %>
|
<% feed_menu.each do |feed| %>
|
||||||
<a href="/feed/<%= feed.downcase %>" class="feed-menu-item pure-menu-heading">
|
<a href="/feed/<%= feed.to_s.downcase %>" class="feed-menu-item pure-menu-heading">
|
||||||
<%= translate(locale, feed) %>
|
<%= translate(locale, feed.to_s) %>
|
||||||
</a>
|
</a>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
@ -40,7 +40,7 @@
|
|||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
<label for="speed"><%= translate(locale, "Default speed: ") %></label>
|
<label for="speed"><%= translate(locale, "Default speed: ") %></label>
|
||||||
<select name="speed" id="speed">
|
<select name="speed" id="speed">
|
||||||
<% {2.0, 1.75, 1.5, 1.25, 1.0, 0.75, 0.5, 0.25}.each do |option| %>
|
<% Settings::ALLOWED_SPEED_VALUES.each do |option| %>
|
||||||
<option <% if preferences.speed == option %> selected <% end %>><%= option %></option>
|
<option <% if preferences.speed == option %> selected <% end %>><%= option %></option>
|
||||||
<% end %>
|
<% end %>
|
||||||
</select>
|
</select>
|
||||||
@ -130,8 +130,8 @@
|
|||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
<label for="player_style"><%= translate(locale, "Player style: ") %></label>
|
<label for="player_style"><%= translate(locale, "Player style: ") %></label>
|
||||||
<select name="player_style" id="player_style">
|
<select name="player_style" id="player_style">
|
||||||
<% {"invidious", "youtube"}.each do |option| %>
|
<% Settings::PlayerStyles.each do |option| %>
|
||||||
<option value="<%= option %>" <% if preferences.player_style == option %> selected <% end %>><%= translate(locale, option) %></option>
|
<option value="<%= option %>" <% if preferences.player_style == option %> selected <% end %>><%= translate(locale, option.to_s.underscore) %></option>
|
||||||
<% end %>
|
<% end %>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@ -139,8 +139,8 @@
|
|||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
<label for="dark_mode"><%= translate(locale, "Theme: ") %></label>
|
<label for="dark_mode"><%= translate(locale, "Theme: ") %></label>
|
||||||
<select name="dark_mode" id="dark_mode">
|
<select name="dark_mode" id="dark_mode">
|
||||||
<% {"", "light", "dark"}.each do |option| %>
|
<% Settings::Themes.each do |option| %>
|
||||||
<option value="<%= option %>" <% if preferences.dark_mode == option %> selected <% end %>><%= translate(locale, option.blank? ? "auto" : option) %></option>
|
<option value="<%= option %>" <% if preferences.dark_mode == option %> selected <% end %>><%= translate(locale, option.to_s.underscore) %></option>
|
||||||
<% end %>
|
<% end %>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@ -150,17 +150,16 @@
|
|||||||
<input name="thin_mode" id="thin_mode" type="checkbox" <% if preferences.thin_mode %>checked<% end %>>
|
<input name="thin_mode" id="thin_mode" type="checkbox" <% if preferences.thin_mode %>checked<% end %>>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% if env.get?("user") %>
|
<%
|
||||||
<% feed_options = {"", "Popular", "Trending", "Subscriptions", "Playlists"} %>
|
feed_options = Settings::HomePages.names
|
||||||
<% else %>
|
feed_options.concat(Settings::UserHomePages.names) if env.get?("user")
|
||||||
<% feed_options = {"", "Popular", "Trending"} %>
|
%>
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
<label for="default_home"><%= translate(locale, "Default homepage: ") %></label>
|
<label for="default_home"><%= translate(locale, "Default homepage: ") %></label>
|
||||||
<select name="default_home" id="default_home">
|
<select name="default_home" id="default_home">
|
||||||
<% feed_options.each do |option| %>
|
<% feed_options.each do |option| %>
|
||||||
<option value="<%= option %>" <% if preferences.default_home == option %> selected <% end %>><%= translate(locale, option.blank? ? "Search" : option) %></option>
|
<option value="<%= option %>" <% if preferences.default_home == option %> selected <% end %>><%= translate(locale, option) %></option>
|
||||||
<% end %>
|
<% end %>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@ -170,11 +169,12 @@
|
|||||||
<% (feed_options.size - 1).times do |index| %>
|
<% (feed_options.size - 1).times do |index| %>
|
||||||
<select name="feed_menu[<%= index %>]" id="feed_menu[<%= index %>]">
|
<select name="feed_menu[<%= index %>]" id="feed_menu[<%= index %>]">
|
||||||
<% feed_options.each do |option| %>
|
<% feed_options.each do |option| %>
|
||||||
<option value="<%= option %>" <% if preferences.feed_menu[index]? == option %> selected <% end %>><%= translate(locale, option.blank? ? "Search" : option) %></option>
|
<option value="<%= option %>" <% if preferences.feed_menu[index]? == option %> selected <% end %>><%= translate(locale, option.to_s) %></option>
|
||||||
<% end %>
|
<% end %>
|
||||||
</select>
|
</select>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% if env.get? "user" %>
|
<% if env.get? "user" %>
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
<label for="show_nick"><%= translate(locale, "Show nickname on top: ") %></label>
|
<label for="show_nick"><%= translate(locale, "Show nickname on top: ") %></label>
|
||||||
|
@ -20,10 +20,10 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<% locale = LOCALES[env.get("preferences").as(Preferences).locale]? %>
|
<% locale = LOCALES[env.get("preferences").as(Preferences).locale]? %>
|
||||||
<% dark_mode = env.get("preferences").as(Preferences).dark_mode %>
|
<% theme = env.get("preferences").as(Preferences).dark_mode %>
|
||||||
|
|
||||||
<body class="<%= dark_mode.blank? ? "no" : dark_mode %>-theme">
|
<body class="<%= theme == Settings::Themes::Auto ? "no" : theme.to_s %>-theme">
|
||||||
<span style="display:none" id="dark_mode_pref"><%= env.get("preferences").as(Preferences).dark_mode %></span>
|
<span style="display:none" id="dark_mode_pref"><%= theme.to_s %></span>
|
||||||
<div class="pure-g">
|
<div class="pure-g">
|
||||||
<div class="pure-u-1 pure-u-md-2-24"></div>
|
<div class="pure-u-1 pure-u-md-2-24"></div>
|
||||||
<div class="pure-u-1 pure-u-md-20-24" id="contents">
|
<div class="pure-u-1 pure-u-md-20-24" id="contents">
|
||||||
@ -45,7 +45,7 @@
|
|||||||
<% if env.get? "user" %>
|
<% if env.get? "user" %>
|
||||||
<div class="pure-u-1-4">
|
<div class="pure-u-1-4">
|
||||||
<a id="toggle_theme" href="/toggle_theme?referer=<%= env.get?("current_page") %>" class="pure-menu-heading">
|
<a id="toggle_theme" href="/toggle_theme?referer=<%= env.get?("current_page") %>" class="pure-menu-heading">
|
||||||
<% if env.get("preferences").as(Preferences).dark_mode == "dark" %>
|
<% if theme == Settings::Themes::Dark %>
|
||||||
<i class="icon ion-ios-sunny"></i>
|
<i class="icon ion-ios-sunny"></i>
|
||||||
<% else %>
|
<% else %>
|
||||||
<i class="icon ion-ios-moon"></i>
|
<i class="icon ion-ios-moon"></i>
|
||||||
@ -83,7 +83,7 @@
|
|||||||
<% else %>
|
<% else %>
|
||||||
<div class="pure-u-1-3">
|
<div class="pure-u-1-3">
|
||||||
<a id="toggle_theme" href="/toggle_theme?referer=<%= env.get?("current_page") %>" class="pure-menu-heading">
|
<a id="toggle_theme" href="/toggle_theme?referer=<%= env.get?("current_page") %>" class="pure-menu-heading">
|
||||||
<% if env.get("preferences").as(Preferences).dark_mode == "dark" %>
|
<% if theme == Settings::Themes::Dark %>
|
||||||
<i class="icon ion-ios-sunny"></i>
|
<i class="icon ion-ios-sunny"></i>
|
||||||
<% else %>
|
<% else %>
|
||||||
<i class="icon ion-ios-moon"></i>
|
<i class="icon ion-ios-moon"></i>
|
||||||
|
Loading…
Reference in New Issue
Block a user