summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorOmar Roth <omarroth@hotmail.com>2019-03-01 16:06:45 -0600
committerOmar Roth <omarroth@hotmail.com>2019-03-01 16:06:45 -0600
commita39b1583dacff31664830fd4f8966014f8869fa0 (patch)
treea2f137aef43680d918c60b33215cee9c7c03d8e9 /src
parent2fe545e19a9e3669b5c4b7b5c53e559afb864341 (diff)
downloadinvidious-a39b1583dacff31664830fd4f8966014f8869fa0.tar.gz
invidious-a39b1583dacff31664830fd4f8966014f8869fa0.tar.bz2
invidious-a39b1583dacff31664830fd4f8966014f8869fa0.zip
Add administrator preferences
Diffstat (limited to 'src')
-rw-r--r--src/invidious.cr239
-rw-r--r--src/invidious/helpers/helpers.cr19
-rw-r--r--src/invidious/jobs.cr6
-rw-r--r--src/invidious/users.cr6
-rw-r--r--src/invidious/views/components/feed_menu.ecr10
-rw-r--r--src/invidious/views/index.ecr14
-rw-r--r--src/invidious/views/login.ecr56
-rw-r--r--src/invidious/views/popular.ecr2
-rw-r--r--src/invidious/views/preferences.ecr82
-rw-r--r--src/invidious/views/template.ecr2
-rw-r--r--src/invidious/views/top.ecr2
-rw-r--r--src/invidious/views/trending.ecr2
12 files changed, 266 insertions, 174 deletions
diff --git a/src/invidious.cr b/src/invidious.cr
index e6e8340b..36450676 100644
--- a/src/invidious.cr
+++ b/src/invidious.cr
@@ -31,42 +31,38 @@ require "./invidious/*"
CONFIG = Config.from_yaml(File.read("config/config.yml"))
HMAC_KEY = CONFIG.hmac_key || Random::Secure.random_bytes(32)
-crawl_threads = CONFIG.crawl_threads
-channel_threads = CONFIG.channel_threads
-feed_threads = CONFIG.feed_threads
-video_threads = CONFIG.video_threads
-
+config = CONFIG
logger = Invidious::LogHandler.new
Kemal.config.extra_options do |parser|
parser.banner = "Usage: invidious [arguments]"
- parser.on("-t THREADS", "--crawl-threads=THREADS", "Number of threads for crawling YouTube (default: #{crawl_threads})") do |number|
+ parser.on("-t THREADS", "--crawl-threads=THREADS", "Number of threads for crawling YouTube (default: #{config.crawl_threads})") do |number|
begin
- crawl_threads = number.to_i
+ config.crawl_threads = number.to_i
rescue ex
puts "THREADS must be integer"
exit
end
end
- parser.on("-c THREADS", "--channel-threads=THREADS", "Number of threads for refreshing channels (default: #{channel_threads})") do |number|
+ parser.on("-c THREADS", "--channel-threads=THREADS", "Number of threads for refreshing channels (default: #{config.channel_threads})") do |number|
begin
- channel_threads = number.to_i
+ config.channel_threads = number.to_i
rescue ex
puts "THREADS must be integer"
exit
end
end
- parser.on("-f THREADS", "--feed-threads=THREADS", "Number of threads for refreshing feeds (default: #{feed_threads})") do |number|
+ parser.on("-f THREADS", "--feed-threads=THREADS", "Number of threads for refreshing feeds (default: #{config.feed_threads})") do |number|
begin
- feed_threads = number.to_i
+ config.feed_threads = number.to_i
rescue ex
puts "THREADS must be integer"
exit
end
end
- parser.on("-v THREADS", "--video-threads=THREADS", "Number of threads for refreshing videos (default: #{video_threads})") do |number|
+ parser.on("-v THREADS", "--video-threads=THREADS", "Number of threads for refreshing videos (default: #{config.video_threads})") do |number|
begin
- video_threads = number.to_i
+ config.video_threads = number.to_i
rescue ex
puts "THREADS must be integer"
exit
@@ -107,28 +103,30 @@ LOCALES = {
"ru" => load_locale("ru"),
}
-crawl_threads.times do
+config.crawl_threads.times do
spawn do
crawl_videos(PG_DB, logger)
end
end
-refresh_channels(PG_DB, logger, channel_threads, CONFIG.full_refresh)
+refresh_channels(PG_DB, logger, config.channel_threads, config.full_refresh)
-refresh_feeds(PG_DB, logger, feed_threads)
+refresh_feeds(PG_DB, logger, config.feed_threads)
-video_threads.times do |i|
+config.video_threads.times do |i|
spawn do
refresh_videos(PG_DB, logger)
end
end
top_videos = [] of Video
-spawn do
- pull_top_videos(CONFIG, PG_DB) do |videos|
- top_videos = videos
- sleep 1.minutes
- Fiber.yield
+if config.top_enabled
+ spawn do
+ pull_top_videos(config, PG_DB) do |videos|
+ top_videos = videos
+ sleep 1.minutes
+ Fiber.yield
+ end
end
end
@@ -231,7 +229,20 @@ get "/" do |env|
end
end
- templated "index"
+ case config.default_home
+ when "Popular"
+ templated "popular"
+ when "Top"
+ templated "top"
+ when "Trending"
+ env.redirect "/feed/trending"
+ when "Subscriptions"
+ if user
+ env.redirect "/feed/subscriptions"
+ else
+ templated "popular"
+ end
+ end
end
get "/licenses" do |env|
@@ -367,7 +378,7 @@ get "/watch" do |env|
video.description = replace_links(video.description)
description = video.short_description
- host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, CONFIG.domain)
+ host_url = make_host_url(Kemal.config.ssl || config.https_only, config.domain)
host_params = env.request.query_params
host_params.delete_all("v")
@@ -467,7 +478,7 @@ get "/embed/:id" do |env|
video.description = replace_links(video.description)
description = video.short_description
- host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, CONFIG.domain)
+ host_url = make_host_url(Kemal.config.ssl || config.https_only, config.domain)
host_params = env.request.query_params
host_params.delete_all("v")
@@ -553,7 +564,7 @@ get "/opensearch.xml" do |env|
locale = LOCALES[env.get("locale").as(String)]?
env.response.content_type = "application/opensearchdescription+xml"
- host = make_host_url(Kemal.config.ssl || CONFIG.https_only, CONFIG.domain)
+ host = make_host_url(Kemal.config.ssl || config.https_only, config.domain)
XML.build(indent: " ", encoding: "UTF-8") do |xml|
xml.element("OpenSearchDescription", xmlns: "http://a9.com/-/spec/opensearch/1.1/") do
@@ -678,6 +689,11 @@ get "/login" do |env|
next env.redirect "/feed/subscriptions"
end
+ if !config.login_enabled
+ error_message = "Login has been disabled by administrator."
+ next templated "error"
+ end
+
referer = get_referer(env, "/feed/subscriptions")
account_type = env.params.query["type"]?
@@ -716,6 +732,11 @@ post "/login" do |env|
referer = get_referer(env, "/feed/subscriptions")
+ if !config.login_enabled
+ error_message = "Login has been disabled by administrator."
+ next templated "error"
+ end
+
email = env.params.body["email"]?
password = env.params.body["password"]?
@@ -876,14 +897,14 @@ post "/login" do |env|
host = URI.parse(env.request.headers["Host"]).host
- if Kemal.config.ssl || CONFIG.https_only
+ if Kemal.config.ssl || config.https_only
secure = true
else
secure = false
end
login.cookies.each do |cookie|
- if Kemal.config.ssl || CONFIG.https_only
+ if Kemal.config.ssl || config.https_only
cookie.secure = secure
else
cookie.secure = secure
@@ -912,54 +933,56 @@ post "/login" do |env|
answer = env.params.body["answer"]?
text_answer = env.params.body["text_answer"]?
- if answer
- answer = answer.lstrip('0')
- answer = OpenSSL::HMAC.hexdigest(:sha256, HMAC_KEY, answer)
-
- challenge = env.params.body["challenge"]?
- token = env.params.body["token"]?
-
- begin
- validate_response(challenge, token, answer, "sign_in", HMAC_KEY, PG_DB, locale)
- rescue ex
- if ex.message == translate(locale, "Invalid user")
- error_message = translate(locale, "Invalid answer")
- else
- error_message = ex.message
- end
-
- next templated "error"
- end
- elsif text_answer
- text_answer = Digest::MD5.hexdigest(text_answer.downcase.strip)
+ if config.captcha_enabled
+ if answer
+ answer = answer.lstrip('0')
+ answer = OpenSSL::HMAC.hexdigest(:sha256, HMAC_KEY, answer)
- challenges = env.params.body.select { |k, v| k.match(/text_challenge\d+/) }
- tokens = env.params.body.select { |k, v| k.match(/text_token\d+/) }
+ challenge = env.params.body["challenge"]?
+ token = env.params.body["token"]?
- found_valid_captcha = false
-
- error_message = translate(locale, "Invalid CAPTCHA")
- challenges.each_with_index do |challenge, i|
begin
- challenge = challenge[1]
- token = tokens[i][1]
- validate_response(challenge, token, text_answer, "sign_in", HMAC_KEY, PG_DB, locale)
- found_valid_captcha = true
+ validate_response(challenge, token, answer, "sign_in", HMAC_KEY, PG_DB, locale)
rescue ex
if ex.message == translate(locale, "Invalid user")
error_message = translate(locale, "Invalid answer")
else
error_message = ex.message
end
+
+ next templated "error"
+ end
+ elsif text_answer
+ text_answer = Digest::MD5.hexdigest(text_answer.downcase.strip)
+
+ challenges = env.params.body.select { |k, v| k.match(/text_challenge\d+/) }
+ tokens = env.params.body.select { |k, v| k.match(/text_token\d+/) }
+
+ found_valid_captcha = false
+
+ error_message = translate(locale, "Invalid CAPTCHA")
+ challenges.each_with_index do |challenge, i|
+ begin
+ challenge = challenge[1]
+ token = tokens[i][1]
+ validate_response(challenge, token, text_answer, "sign_in", HMAC_KEY, PG_DB, locale)
+ found_valid_captcha = true
+ rescue ex
+ if ex.message == translate(locale, "Invalid user")
+ error_message = translate(locale, "Invalid answer")
+ else
+ error_message = ex.message
+ end
+ end
end
- end
- if !found_valid_captcha
+ if !found_valid_captcha
+ next templated "error"
+ end
+ else
+ error_message = translate(locale, "CAPTCHA is a required field")
next templated "error"
end
- else
- error_message = translate(locale, "CAPTCHA is a required field")
- next templated "error"
end
action = env.params.body["action"]?
@@ -992,14 +1015,14 @@ post "/login" do |env|
sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32))
PG_DB.exec("INSERT INTO session_ids VALUES ($1, $2, $3)", sid, email, Time.now)
- if Kemal.config.ssl || CONFIG.https_only
+ if Kemal.config.ssl || config.https_only
secure = true
else
secure = false
end
- if CONFIG.domain
- env.response.cookies["SID"] = HTTP::Cookie.new(name: "SID", domain: ".#{CONFIG.domain}", value: sid, expires: Time.now + 2.years,
+ if config.domain
+ env.response.cookies["SID"] = HTTP::Cookie.new(name: "SID", domain: ".#{config.domain}", value: sid, expires: Time.now + 2.years,
secure: secure, http_only: true)
else
env.response.cookies["SID"] = HTTP::Cookie.new(name: "SID", value: sid, expires: Time.now + 2.years,
@@ -1016,6 +1039,11 @@ post "/login" do |env|
secure: secure, http_only: true)
end
elsif action == "register"
+ if !config.registration_enabled
+ error_message = "Registration has been disabled by administrator."
+ next templated "error"
+ end
+
if password.empty?
error_message = translate(locale, "Password cannot be empty")
next templated "error"
@@ -1049,14 +1077,14 @@ post "/login" do |env|
ucid = ANY ((SELECT subscriptions FROM users WHERE email = E'#{user.email.gsub("'", "\\'")}')::text[]) \
ORDER BY published DESC;")
- if Kemal.config.ssl || CONFIG.https_only
+ if Kemal.config.ssl || config.https_only
secure = true
else
secure = false
end
- if CONFIG.domain
- env.response.cookies["SID"] = HTTP::Cookie.new(name: "SID", domain: ".#{CONFIG.domain}", value: sid, expires: Time.now + 2.years,
+ if config.domain
+ env.response.cookies["SID"] = HTTP::Cookie.new(name: "SID", domain: ".#{config.domain}", value: sid, expires: Time.now + 2.years,
secure: secure, http_only: true)
else
env.response.cookies["SID"] = HTTP::Cookie.new(name: "SID", value: sid, expires: Time.now + 2.years,
@@ -1153,14 +1181,15 @@ post "/preferences" do |env|
volume = env.params.body["volume"]?.try &.as(String).to_i?
volume ||= DEFAULT_USER_PREFERENCES.volume
- comments_0 = env.params.body["comments_0"]?.try &.as(String) || DEFAULT_USER_PREFERENCES.comments[0]
- comments_1 = env.params.body["comments_1"]?.try &.as(String) || DEFAULT_USER_PREFERENCES.comments[1]
- comments = [comments_0, comments_1]
+ comments = [] of String
+ 2.times do |i|
+ comments << (env.params.body["comments[#{i}]"]?.try &.as(String) || DEFAULT_USER_PREFERENCES.comments[i])
+ end
- captions_0 = env.params.body["captions_0"]?.try &.as(String) || DEFAULT_USER_PREFERENCES.captions[0]
- captions_1 = env.params.body["captions_1"]?.try &.as(String) || DEFAULT_USER_PREFERENCES.captions[1]
- captions_2 = env.params.body["captions_2"]?.try &.as(String) || DEFAULT_USER_PREFERENCES.captions[2]
- captions = [captions_0, captions_1, captions_2]
+ captions = [] of String
+ 3.times do |i|
+ captions << (env.params.body["captions[#{i}]"]?.try &.as(String) || DEFAULT_USER_PREFERENCES.captions[i])
+ end
related_videos = env.params.body["related_videos"]?.try &.as(String)
related_videos ||= "off"
@@ -1224,6 +1253,37 @@ post "/preferences" do |env|
if user = env.get? "user"
user = user.as(User)
PG_DB.exec("UPDATE users SET preferences = $1 WHERE email = $2", preferences, user.email)
+
+ if config.admins.includes? user.email
+ config.default_home = env.params.body["default_home"]?.try &.as(String) || config.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
+ config.feed_menu = feed_menu
+
+ top_enabled = env.params.body["top_enabled"]?.try &.as(String)
+ top_enabled ||= "off"
+ config.top_enabled = top_enabled == "on"
+
+ captcha_enabled = env.params.body["captcha_enabled"]?.try &.as(String)
+ captcha_enabled ||= "off"
+ config.captcha_enabled = captcha_enabled == "on"
+
+ login_enabled = env.params.body["login_enabled"]?.try &.as(String)
+ login_enabled ||= "off"
+ config.login_enabled = login_enabled == "on"
+
+ registration_enabled = env.params.body["registration_enabled"]?.try &.as(String)
+ registration_enabled ||= "off"
+ config.registration_enabled = registration_enabled == "on"
+
+ File.write("config/config.yml", config.to_yaml)
+ end
else
env.response.cookies["PREFS"] = preferences
end
@@ -1397,7 +1457,7 @@ get "/subscription_manager" do |env|
subscriptions.sort_by! { |channel| channel.author.downcase }
if action_takeout
- host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, CONFIG.domain)
+ host_url = make_host_url(Kemal.config.ssl || config.https_only, config.domain)
if format == "json"
env.response.content_type = "application/json"
@@ -1741,7 +1801,11 @@ end
get "/feed/top" do |env|
locale = LOCALES[env.get("locale").as(String)]?
- templated "top"
+ if config.top_enabled
+ templated "top"
+ else
+ env.redirect "/"
+ end
end
get "/feed/popular" do |env|
@@ -1984,7 +2048,7 @@ get "/feed/channel/:ucid" do |env|
)
end
- host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, CONFIG.domain)
+ host_url = make_host_url(Kemal.config.ssl || config.https_only, config.domain)
path = env.request.path
feed = XML.build(indent: " ", encoding: "UTF-8") do |xml|
@@ -2118,7 +2182,7 @@ get "/feed/private" do |env|
videos = videos[0..max_results]
end
- host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, CONFIG.domain)
+ host_url = make_host_url(Kemal.config.ssl || config.https_only, config.domain)
path = env.request.path
query = env.request.query.not_nil!
@@ -2173,7 +2237,7 @@ get "/feed/playlist/:plid" do |env|
plid = env.params.url["plid"]
- host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, CONFIG.domain)
+ host_url = make_host_url(Kemal.config.ssl || config.https_only, config.domain)
path = env.request.path
client = make_client(YT_URL)
@@ -2487,7 +2551,7 @@ get "/api/v1/insights/:id" do |env|
env.response.content_type = "application/json"
error_message = {"error" => "YouTube has removed publicly-available analytics."}.to_json
- halt env, status_code: 503, response: error_message
+ halt env, status_code: 410, response: error_message
client = make_client(YT_URL)
headers = HTTP::Headers.new
@@ -2653,7 +2717,7 @@ get "/api/v1/videos/:id" do |env|
end
if video.player_response["streamingData"]?.try &.["hlsManifestUrl"]?
- host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, CONFIG.domain)
+ host_url = make_host_url(Kemal.config.ssl || config.https_only, config.domain)
host_params = env.request.query_params
host_params.delete_all("v")
@@ -2871,6 +2935,11 @@ get "/api/v1/top" do |env|
env.response.content_type = "application/json"
+ if !config.top_enabled
+ error_message = {"error" => "Administrator has disabled this endpoint."}.to_json
+ halt env, status_code: 400, response: error_message
+ end
+
videos = JSON.build do |json|
json.array do
top_videos.each do |video|
@@ -3842,7 +3911,7 @@ get "/api/manifest/hls_variant/*" do |env|
env.response.content_type = "application/x-mpegURL"
env.response.headers.add("Access-Control-Allow-Origin", "*")
- host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, CONFIG.domain)
+ host_url = make_host_url(Kemal.config.ssl || config.https_only, config.domain)
manifest = manifest.body
manifest.gsub("https://www.youtube.com", host_url)
@@ -3856,7 +3925,7 @@ get "/api/manifest/hls_playlist/*" do |env|
halt env, status_code: manifest.status_code
end
- host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, CONFIG.domain)
+ host_url = make_host_url(Kemal.config.ssl || config.https_only, config.domain)
manifest = manifest.body.gsub("https://www.youtube.com", host_url)
manifest = manifest.gsub(/https:\/\/r\d---.{11}\.c\.youtube\.com/, host_url)
diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr
index 45ebc4dd..26b6244c 100644
--- a/src/invidious/helpers/helpers.cr
+++ b/src/invidious/helpers/helpers.cr
@@ -1,9 +1,9 @@
class Config
YAML.mapping({
+ video_threads: Int32, # Number of threads to use for updating videos in cache (mostly non-functional)
crawl_threads: Int32, # Number of threads to use for finding new videos from YouTube (used to populate "top" page)
channel_threads: Int32, # Number of threads to use for crawling videos from channels (for updating subscriptions)
feed_threads: Int32, # Number of threads to use for updating feeds
- video_threads: Int32, # Number of threads to use for updating videos in cache (mostly non-functional)
db: NamedTuple( # Database configuration
user: String,
password: String,
@@ -11,11 +11,18 @@ user: String,
port: Int32,
dbname: String,
),
- dl_api_key: String?, # DetectLanguage API Key (used to filter non-English results from "top" page), mostly non-functional
- https_only: Bool?, # Used to tell Invidious it is behind a proxy, so links to resources should be https://
- hmac_key: String?, # HMAC signing key for CSRF tokens
- full_refresh: Bool, # Used for crawling channels: threads should check all videos uploaded by a channel
- domain: String, # Domain to be used for links to resources on the site where an absolute URL is required
+ full_refresh: Bool, # Used for crawling channels: threads should check all videos uploaded by a channel
+ https_only: Bool?, # Used to tell Invidious it is behind a proxy, so links to resources should be https://
+ hmac_key: String?, # HMAC signing key for CSRF tokens
+ domain: String, # Domain to be used for links to resources on the site where an absolute URL is required
+ dl_api_key: String?, # DetectLanguage API Key (used to filter non-English results from "top" page), mostly non-functional
+ default_home: {type: String, default: "Top"},
+ feed_menu: {type: Array(String), default: ["Popular", "Top", "Trending"]},
+ top_enabled: {type: Bool, default: true},
+ captcha_enabled: {type: Bool, default: true},
+ login_enabled: {type: Bool, default: true},
+ registration_enabled: {type: Bool, default: true},
+ admins: {type: Array(String), default: [] of String},
})
end
diff --git a/src/invidious/jobs.cr b/src/invidious/jobs.cr
index e000540a..cc5f85d1 100644
--- a/src/invidious/jobs.cr
+++ b/src/invidious/jobs.cr
@@ -133,7 +133,7 @@ def refresh_feeds(db, logger, max_threads = 1)
rescue ex
# Create view if it doesn't exist
if ex.message.try &.ends_with? "does not exist"
- PG_DB.exec("CREATE MATERIALIZED VIEW #{view_name} AS \
+ db.exec("CREATE MATERIALIZED VIEW #{view_name} AS \
SELECT * FROM channel_videos WHERE \
ucid = ANY ((SELECT subscriptions FROM users WHERE email = E'#{email.gsub("'", "\\'")}')::text[]) \
ORDER BY published DESC;")
@@ -193,11 +193,11 @@ end
def pull_popular_videos(db)
loop do
- subscriptions = PG_DB.query_all("SELECT channel FROM \
+ subscriptions = db.query_all("SELECT channel FROM \
(SELECT UNNEST(subscriptions) AS channel FROM users) AS d \
GROUP BY channel ORDER BY COUNT(channel) DESC LIMIT 40", as: String)
- videos = PG_DB.query_all("SELECT DISTINCT ON (ucid) * FROM \
+ videos = db.query_all("SELECT DISTINCT ON (ucid) * FROM \
channel_videos WHERE ucid IN (#{arg_array(subscriptions)}) \
ORDER BY ucid, published DESC", subscriptions, as: ChannelVideo).sort_by { |video| video.published }.reverse
diff --git a/src/invidious/users.cr b/src/invidious/users.cr
index 48d8008f..42468228 100644
--- a/src/invidious/users.cr
+++ b/src/invidious/users.cr
@@ -143,7 +143,7 @@ def get_user(sid, headers, db, refresh = true)
begin
view_name = "subscriptions_#{sha256(user.email)[0..7]}"
- PG_DB.exec("CREATE MATERIALIZED VIEW #{view_name} AS \
+ db.exec("CREATE MATERIALIZED VIEW #{view_name} AS \
SELECT * FROM channel_videos WHERE \
ucid = ANY ((SELECT subscriptions FROM users WHERE email = E'#{user.email.gsub("'", "\\'")}')::text[]) \
ORDER BY published DESC;")
@@ -165,7 +165,7 @@ def get_user(sid, headers, db, refresh = true)
begin
view_name = "subscriptions_#{sha256(user.email)[0..7]}"
- PG_DB.exec("CREATE MATERIALIZED VIEW #{view_name} AS \
+ db.exec("CREATE MATERIALIZED VIEW #{view_name} AS \
SELECT * FROM channel_videos WHERE \
ucid = ANY ((SELECT subscriptions FROM users WHERE email = E'#{user.email.gsub("'", "\\'")}')::text[]) \
ORDER BY published DESC;")
@@ -247,7 +247,7 @@ def validate_response(challenge, token, user_id, operation, key, db, locale)
raise translate(locale, "Invalid challenge")
end
- challenge = OpenSSL::HMAC.digest(:sha256, HMAC_KEY, challenge)
+ challenge = OpenSSL::HMAC.digest(:sha256, key, challenge)
challenge = Base64.urlsafe_encode(challenge)
if db.query_one?("SELECT EXISTS (SELECT true FROM nonces WHERE nonce = $1)", nonce, as: Bool)
diff --git a/src/invidious/views/components/feed_menu.ecr b/src/invidious/views/components/feed_menu.ecr
index 5c19bafc..378601a1 100644
--- a/src/invidious/views/components/feed_menu.ecr
+++ b/src/invidious/views/components/feed_menu.ecr
@@ -2,12 +2,12 @@
<div class="pure-u-1 pure-u-md-1-4"></div>
<div class="pure-u-1 pure-u-md-1-2">
<div class="pure-g">
- <% feeds = ["Popular", "Top", "Trending"] %>
- <% if env.get? "user" %>
- <% feeds << "Subscriptions" %>
+ <% feed_menu = config.feed_menu.dup %>
+ <% if !env.get?("user") %>
+ <% feed_menu.reject! {|feed| feed == "Subscriptions"} %>
<% end %>
- <% feeds.each do |feed| %>
- <div class="pure-u-1-2 pure-u-md-1-<%= feeds.size %>">
+ <% feed_menu.each do |feed| %>
+ <div class="pure-u-1-2 pure-u-md-1-<%= feed_menu.size %>">
<a href="/feed/<%= feed.downcase %>" style="text-align:center;" class="pure-menu-heading">
<%= translate(locale, feed) %>
</a>
diff --git a/src/invidious/views/index.ecr b/src/invidious/views/index.ecr
deleted file mode 100644
index 6cc978e5..00000000
--- a/src/invidious/views/index.ecr
+++ /dev/null
@@ -1,14 +0,0 @@
-<% content_for "header" do %>
-<meta name="description" content="<%= translate(locale, "An alternative front-end to YouTube") %>">
-<title>Invidious</title>
-<% end %>
-
-<%= rendered "components/feed_menu" %>
-
-<div class="pure-g">
-<% top_videos.each_slice(4) do |slice| %>
- <% slice.each do |item| %>
- <%= rendered "components/item" %>
- <% end %>
-<% end %>
-</div>
diff --git a/src/invidious/views/login.ecr b/src/invidious/views/login.ecr
index f4b0ce96..20363b2c 100644
--- a/src/invidious/views/login.ecr
+++ b/src/invidious/views/login.ecr
@@ -27,36 +27,40 @@
<label for="password"><%= translate(locale, "Password:") %></label>
<input required class="pure-input-1" name="password" type="password" placeholder="Password">
-
- <% if captcha_type == "image" %>
- <img style="width:100%" src='<%= captcha.not_nil![:image] %>'/>
- <input type="hidden" name="token" value="<%= captcha.not_nil![:token] %>">
- <input type="hidden" name="challenge" value="<%= captcha.not_nil![:challenge] %>">
- <label for="answer"><%= translate(locale, "Time (h:mm:ss):") %></label>
- <input required type="text" name="answer" type="text" placeholder="h:mm:ss">
-
- <label>
- <a href="/login?referer=<%= URI.escape(referer) %>&type=invidious&captcha=text">
- <%= translate(locale, "Text CAPTCHA") %>
- </a>
- </label>
- <% else %>
- <% text_captcha.not_nil![:tokens].each_with_index do |token, i| %>
- <input type="hidden" name="text_challenge<%= i %>" value="<%= token[0] %>">
- <input type="hidden" name="text_token<%= i %>" value="<%= token[1] %>">
- <% end %>
- <label for="text_answer"><%= text_captcha.not_nil![:question] %></label>
- <input required type="text" name="text_answer" type="text" placeholder="Answer">
- <label>
- <a href="/login?referer=<%= URI.escape(referer) %>&type=invidious">
- <%= translate(locale, "Image CAPTCHA") %>
- </a>
- </label>
+ <% if config.captcha_enabled %>
+ <% if captcha_type == "image" %>
+ <img style="width:100%" src='<%= captcha.not_nil![:image] %>'/>
+ <input type="hidden" name="token" value="<%= captcha.not_nil![:token] %>">
+ <input type="hidden" name="challenge" value="<%= captcha.not_nil![:challenge] %>">
+ <label for="answer"><%= translate(locale, "Time (h:mm:ss):") %></label>
+ <input required type="text" name="answer" type="text" placeholder="h:mm:ss">
+
+ <label>
+ <a href="/login?referer=<%= URI.escape(referer) %>&type=invidious&captcha=text">
+ <%= translate(locale, "Text CAPTCHA") %>
+ </a>
+ </label>
+ <% else %>
+ <% text_captcha.not_nil![:tokens].each_with_index do |token, i| %>
+ <input type="hidden" name="text_challenge<%= i %>" value="<%= token[0] %>">
+ <input type="hidden" name="text_token<%= i %>" value="<%= token[1] %>">
+ <% end %>
+ <label for="text_answer"><%= text_captcha.not_nil![:question] %></label>
+ <input required type="text" name="text_answer" type="text" placeholder="Answer">
+
+ <label>
+ <a href="/login?referer=<%= URI.escape(referer) %>&type=invidious">
+ <%= translate(locale, "Image CAPTCHA") %>
+ </a>
+ </label>
+ <% end %>
<% end %>
<button type="submit" name="action" value="signin" class="pure-button pure-button-primary"><%= translate(locale, "Sign In") %></button>
+ <% if config.registration_enabled %>
<button type="submit" name="action" value="register" class="pure-button pure-button-primary"><%= translate(locale, "Register") %></button>
+ <% end %>
</fieldset>
</form>
<% elsif account_type == "google" %>
@@ -67,7 +71,7 @@
<label for="password"><%= translate(locale, "Password:") %></label>
<input required class="pure-input-1" name="password" type="password" placeholder="Password">
-
+
<% if tfa %>
<label for="tfa"><%= translate(locale, "Google verification code:") %></label>
<input required class="pure-input-1" name="tfa" type="text" placeholder="Google verification code">
diff --git a/src/invidious/views/popular.ecr b/src/invidious/views/popular.ecr
index f235aad8..d60ae557 100644
--- a/src/invidious/views/popular.ecr
+++ b/src/invidious/views/popular.ecr
@@ -1,6 +1,6 @@
<% content_for "header" do %>
<meta name="description" content="<%= translate(locale, "An alternative front-end to YouTube") %>">
-<title><%= translate(locale, "Popular") %> - Invidious</title>
+<title><% if config.default_home != "Popular" %><%= translate(locale, "Popular") %> - <% end %>Invidious</title>
<% end %>
<%= rendered "components/feed_menu" %>
diff --git a/src/invidious/views/preferences.ecr b/src/invidious/views/preferences.ecr
index 7435c91e..ea6a84c8 100644
--- a/src/invidious/views/preferences.ecr
+++ b/src/invidious/views/preferences.ecr
@@ -58,45 +58,25 @@ function update_value(element) {
</div>
<div class="pure-control-group">
- <label for="comments_0"><%= translate(locale, "Default comments: ") %></label>
- <select name="comments_0" id="comments_0">
+ <label for="comments[0]"><%= translate(locale, "Default comments: ") %></label>
+ <% preferences.comments.each_with_index do |comments, index| %>
+ <select name="comments[<%= index %>]" id="comments[<%= index %>]">
<% {"", "youtube", "reddit"}.each do |option| %>
- <option value="<%= option %>" <% if preferences.comments[0] == option %> selected <% end %>><%= translate(locale, option) %></option>
+ <option value="<%= option %>" <% if preferences.comments[index] == option %> selected <% end %>><%= translate(locale, option) %></option>
<% end %>
</select>
- </div>
-
- <div class="pure-control-group">
- <label for="comments_1"><%= translate(locale, "Fallback comments: ") %></label>
- <select name="comments_1" id="comments_1">
- <% {"", "youtube", "reddit"}.each do |option| %>
- <option value="<%= option %>" <% if preferences.comments[1] == option %> selected <% end %>><%= translate(locale, option) %></option>
- <% end %>
- </select>
- </div>
-
- <div class="pure-control-group">
- <label for="captions_0"><%= translate(locale, "Default captions: ") %></label>
- <select class="pure-u-1-5" name="captions_0" id="captions_0">
- <% CAPTION_LANGUAGES.each do |option| %>
- <option value="<%= option %>" <% if preferences.captions[0] == option %> selected <% end %>><%= translate(locale, option) %></option>
<% end %>
- </select>
</div>
<div class="pure-control-group">
- <label for="captions_fallback"><%= translate(locale, "Fallback captions: ") %></label>
- <select class="pure-u-1-5" name="captions_1" id="captions_1">
+ <label for="captions[0]"><%= translate(locale, "Default captions: ") %></label>
+ <% preferences.captions.each_with_index do |caption, index| %>
+ <select class="pure-u-1-6" name="captions[<%= index %>]" id="captions[<%= index %>]">
<% CAPTION_LANGUAGES.each do |option| %>
- <option value="<%= option %>" <% if preferences.captions[1] == option %> selected <% end %>><%= translate(locale, option) %></option>
+ <option value="<%= option %>" <% if preferences.captions[index] == option %> selected <% end %>><%= translate(locale, option) %></option>
<% end %>
</select>
-
- <select class="pure-u-1-5" name="captions_2" id="captions_2">
- <% CAPTION_LANGUAGES.each do |option| %>
- <option value="<%= option %>" <% if preferences.captions[2] == option %> selected <% end %>><%= translate(locale, option) %></option>
<% end %>
- </select>
</div>
<div class="pure-control-group">
@@ -167,13 +147,57 @@ function update_value(element) {
</div>
<% end %>
+ <% if env.get?("user") && config.admins.includes? env.get?("user").as(User).email %>
+ <legend><%= translate(locale, "Administrator preferences") %></legend>
+
+ <div class="pure-control-group">
+ <label for="default_home"><%= translate(locale, "Default homepage: ") %></label>
+ <select name="default_home" id="default_home">
+ <% {"Popular", "Top", "Trending", "Subscriptions"}.each do |option| %>
+ <option value="<%= option %>" <% if config.default_home == option %> selected <% end %>><%= translate(locale, option) %></option>
+ <% end %>
+ </select>
+ </div>
+
+ <div class="pure-control-group">
+ <label for="feed_menu"><%= translate(locale, "Feed menu: ") %></label>
+ <% 4.times do |index| %>
+ <select name="feed_menu[<%= index %>]" id="feed_menu[<%= index %>]">
+ <% {"", "Popular", "Top", "Trending", "Subscriptions"}.each do |option| %>
+ <option value="<%= option %>" <% if config.feed_menu[index]? == option %> selected <% end %>><%= translate(locale, option) %></option>
+ <% end %>
+ </select>
+ <% end %>
+ </div>
+
+ <div class="pure-control-group">
+ <label for="top_enabled"><%= translate(locale, "Top enabled? ") %></label>
+ <input name="top_enabled" id="top_enabled" type="checkbox" <% if config.top_enabled %>checked<% end %>>
+ </div>
+
+ <div class="pure-control-group">
+ <label for="captcha_enabled"><%= translate(locale, "CAPTCHA enabled? ") %></label>
+ <input name="captcha_enabled" id="captcha_enabled" type="checkbox" <% if config.captcha_enabled %>checked<% end %>>
+ </div>
+
+ <div class="pure-control-group">
+ <label for="login_enabled"><%= translate(locale, "Login enabled? ") %></label>
+ <input name="login_enabled" id="login_enabled" type="checkbox" <% if config.login_enabled %>checked<% end %>>
+ </div>
+
+ <div class="pure-control-group">
+ <label for="registration_enabled"><%= translate(locale, "Registration enabled? ") %></label>
+ <input name="registration_enabled" id="registration_enabled" type="checkbox" <% if config.registration_enabled %>checked<% end %>>
+ </div>
+ <% end %>
+
<% if env.get? "user" %>
<legend><%= translate(locale, "Data preferences") %></legend>
<div class="pure-control-group">
<a href="/clear_watch_history?referer=<%= URI.escape(referer) %>"><%= translate(locale, "Clear watch history") %></a>
</div>
-
+
<div class="pure-control-group">
<a href="/data_control?referer=<%= URI.escape(referer) %>"><%= translate(locale, "Import/Export data") %></a>
</div>
diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr
index b58135d7..0ca794ca 100644
--- a/src/invidious/views/template.ecr
+++ b/src/invidious/views/template.ecr
@@ -89,12 +89,14 @@
<i class="icon ion-ios-cog"></i>
</a>
</div>
+ <% if config.login_enabled %>
<div class="pure-u-1-3">
<a href="/login?referer=<%= env.get?("current_page") %>" class="pure-menu-heading">
<%= translate(locale, "Login") %>
</a>
</div>
<% end %>
+ <% end %>
</div>
</div>
<%= content %>
diff --git a/src/invidious/views/top.ecr b/src/invidious/views/top.ecr
index acf122a6..44faf706 100644
--- a/src/invidious/views/top.ecr
+++ b/src/invidious/views/top.ecr
@@ -1,6 +1,6 @@
<% content_for "header" do %>
<meta name="description" content="<%= translate(locale, "An alternative front-end to YouTube") %>">
-<title><%= translate(locale, "Top") %> - Invidious</title>
+<title><% if config.default_home != "Top" %><%= translate(locale, "Top") %> - <% end %>Invidious</title>
<% end %>
<%= rendered "components/feed_menu" %>
diff --git a/src/invidious/views/trending.ecr b/src/invidious/views/trending.ecr
index 617a9a58..efd9999a 100644
--- a/src/invidious/views/trending.ecr
+++ b/src/invidious/views/trending.ecr
@@ -1,6 +1,6 @@
<% content_for "header" do %>
<meta name="description" content="<%= translate(locale, "An alternative front-end to YouTube") %>">
-<title><%= translate(locale, "Trending") %> - Invidious</title>
+<title><% if config.default_home != "Trending" %><%= translate(locale, "Trending") %> - <% end %>Invidious</title>
<% end %>
<%= rendered "components/feed_menu" %>