Add administrator preferences

This commit is contained in:
Omar Roth 2019-03-01 16:06:45 -06:00
parent 2fe545e19a
commit a39b1583da
22 changed files with 616 additions and 454 deletions

View file

@ -82,6 +82,13 @@
"Manage subscriptions": "إدارة المشتركين", "Manage subscriptions": "إدارة المشتركين",
"Watch history": "سجل المشاهدة", "Watch history": "سجل المشاهدة",
"Delete account": "حذف الحساب", "Delete account": "حذف الحساب",
"Administrator preferences": "",
"Default homepage: ": "",
"Feed menu: ": "",
"Top enabled? ": "",
"CAPTCHA enabled? ": "",
"Login enabled? ": "",
"Registration enabled? ": "",
"Save preferences": "حفظ التفضيلات", "Save preferences": "حفظ التفضيلات",
"Subscription manager": "مدير الإشتراكات", "Subscription manager": "مدير الإشتراكات",
"`x` subscriptions": "`x` مشتركين", "`x` subscriptions": "`x` مشتركين",

View file

@ -82,6 +82,13 @@
"Manage subscriptions": "Abonnements verwalten", "Manage subscriptions": "Abonnements verwalten",
"Watch history": "Verlauf", "Watch history": "Verlauf",
"Delete account": "Account löschen", "Delete account": "Account löschen",
"Administrator preferences": "",
"Default homepage: ": "",
"Feed menu: ": "",
"Top enabled? ": "",
"CAPTCHA enabled? ": "",
"Login enabled? ": "",
"Registration enabled? ": "",
"Save preferences": "Einstellungen speichern", "Save preferences": "Einstellungen speichern",
"Subscription manager": "Abonnementverwaltung", "Subscription manager": "Abonnementverwaltung",
"`x` subscriptions": "`x` Abonnements", "`x` subscriptions": "`x` Abonnements",

View file

@ -80,6 +80,13 @@
"Manage subscriptions": "Manage subscriptions", "Manage subscriptions": "Manage subscriptions",
"Watch history": "Watch history", "Watch history": "Watch history",
"Delete account": "Delete account", "Delete account": "Delete account",
"Administrator preferences": "Administrator preferences",
"Default homepage: ": "Default homepage: ",
"Feed menu: ": "Feed menu: ",
"Top enabled? ": "Top enabled? ",
"CAPTCHA enabled? ": "CAPTCHA enabled? ",
"Login enabled? ": "Login enabled? ",
"Registration enabled? ": "Registration enabled? ",
"Save preferences": "Save preferences", "Save preferences": "Save preferences",
"Subscription manager": "Subscription manager", "Subscription manager": "Subscription manager",
"`x` subscriptions": "`x` subscriptions", "`x` subscriptions": "`x` subscriptions",

View file

@ -80,6 +80,13 @@
"Manage subscriptions": "", "Manage subscriptions": "",
"Watch history": "", "Watch history": "",
"Delete account": "", "Delete account": "",
"Administrator preferences": "",
"Default homepage: ": "",
"Feed menu: ": "",
"Top enabled? ": "",
"CAPTCHA enabled? ": "",
"Login enabled? ": "",
"Registration enabled? ": "",
"Save preferences": "", "Save preferences": "",
"Subscription manager": "", "Subscription manager": "",
"`x` subscriptions": "", "`x` subscriptions": "",

View file

@ -79,6 +79,13 @@
"Manage subscriptions": "Gérer les abonnements", "Manage subscriptions": "Gérer les abonnements",
"Watch history": "Historique de visionnage", "Watch history": "Historique de visionnage",
"Delete account": "Supprimer votre compte", "Delete account": "Supprimer votre compte",
"Administrator preferences": "",
"Default homepage: ": "",
"Feed menu: ": "",
"Top enabled? ": "",
"CAPTCHA enabled? ": "",
"Login enabled? ": "",
"Registration enabled? ": "",
"Save preferences": "Enregistrer les préférences", "Save preferences": "Enregistrer les préférences",
"Subscription manager": "Gestionnaire d'abonnement", "Subscription manager": "Gestionnaire d'abonnement",
"`x` subscriptions": "`x` abonnements", "`x` subscriptions": "`x` abonnements",

View file

@ -79,6 +79,13 @@
"Manage subscriptions": "Gestisci le iscrizioni", "Manage subscriptions": "Gestisci le iscrizioni",
"Watch history": "Cronologia dei video", "Watch history": "Cronologia dei video",
"Delete account": "Elimina l'account", "Delete account": "Elimina l'account",
"Administrator preferences": "",
"Default homepage: ": "",
"Feed menu: ": "",
"Top enabled? ": "",
"CAPTCHA enabled? ": "",
"Login enabled? ": "",
"Registration enabled? ": "",
"Save preferences": "Salva le preferenze", "Save preferences": "Salva le preferenze",
"Subscription manager": "Gestisci le iscrizioni", "Subscription manager": "Gestisci le iscrizioni",
"`x` subscriptions": "`x` iscrizioni", "`x` subscriptions": "`x` iscrizioni",

View file

@ -80,6 +80,13 @@
"Manage subscriptions": "Behandle abonnementer", "Manage subscriptions": "Behandle abonnementer",
"Watch history": "Visningshistorikk", "Watch history": "Visningshistorikk",
"Delete account": "Slett konto", "Delete account": "Slett konto",
"Administrator preferences": "",
"Default homepage: ": "",
"Feed menu: ": "",
"Top enabled? ": "",
"CAPTCHA enabled? ": "",
"Login enabled? ": "",
"Registration enabled? ": "",
"Save preferences": "Lagre innstillinger", "Save preferences": "Lagre innstillinger",
"Subscription manager": "Abonnementsbehandler", "Subscription manager": "Abonnementsbehandler",
"`x` subscriptions": "`x` abonnementer", "`x` subscriptions": "`x` abonnementer",

View file

@ -80,6 +80,13 @@
"Manage subscriptions": "Abonnees beheren", "Manage subscriptions": "Abonnees beheren",
"Watch history": "Kijkgeschiedenis", "Watch history": "Kijkgeschiedenis",
"Delete account": "Account verwijderen", "Delete account": "Account verwijderen",
"Administrator preferences": "",
"Default homepage: ": "",
"Feed menu: ": "",
"Top enabled? ": "",
"CAPTCHA enabled? ": "",
"Login enabled? ": "",
"Registration enabled? ": "",
"Save preferences": "Opslaan voorkeuren", "Save preferences": "Opslaan voorkeuren",
"Subscription manager": "Abonnees beheerder", "Subscription manager": "Abonnees beheerder",
"`x` subscriptions": "`x` abonnees", "`x` subscriptions": "`x` abonnees",

View file

@ -80,6 +80,13 @@
"Manage subscriptions": "Organizuj subskrybcje", "Manage subscriptions": "Organizuj subskrybcje",
"Watch history": "Historia", "Watch history": "Historia",
"Delete account": "Usuń konto", "Delete account": "Usuń konto",
"Administrator preferences": "",
"Default homepage: ": "",
"Feed menu: ": "",
"Top enabled? ": "",
"CAPTCHA enabled? ": "",
"Login enabled? ": "",
"Registration enabled? ": "",
"Save preferences": "Zapisz preferencje", "Save preferences": "Zapisz preferencje",
"Subscription manager": "Manager subskrybcji", "Subscription manager": "Manager subskrybcji",
"`x` subscriptions": "`x` subskrybcji", "`x` subscriptions": "`x` subskrybcji",

View file

@ -82,6 +82,13 @@
"Manage subscriptions": "Управление подписками", "Manage subscriptions": "Управление подписками",
"Watch history": "История просмотров", "Watch history": "История просмотров",
"Delete account": "Удалить аккаунт", "Delete account": "Удалить аккаунт",
"Administrator preferences": "",
"Default homepage: ": "",
"Feed menu: ": "",
"Top enabled? ": "",
"CAPTCHA enabled? ": "",
"Login enabled? ": "",
"Registration enabled? ": "",
"Save preferences": "Сохранить настройки", "Save preferences": "Сохранить настройки",
"Subscription manager": "Менеджер подписок", "Subscription manager": "Менеджер подписок",
"`x` subscriptions": "`x` подписок", "`x` subscriptions": "`x` подписок",

View file

@ -31,42 +31,38 @@ require "./invidious/*"
CONFIG = Config.from_yaml(File.read("config/config.yml")) CONFIG = Config.from_yaml(File.read("config/config.yml"))
HMAC_KEY = CONFIG.hmac_key || Random::Secure.random_bytes(32) HMAC_KEY = CONFIG.hmac_key || Random::Secure.random_bytes(32)
crawl_threads = CONFIG.crawl_threads config = CONFIG
channel_threads = CONFIG.channel_threads
feed_threads = CONFIG.feed_threads
video_threads = CONFIG.video_threads
logger = Invidious::LogHandler.new logger = Invidious::LogHandler.new
Kemal.config.extra_options do |parser| Kemal.config.extra_options do |parser|
parser.banner = "Usage: invidious [arguments]" 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 begin
crawl_threads = number.to_i config.crawl_threads = number.to_i
rescue ex rescue ex
puts "THREADS must be integer" puts "THREADS must be integer"
exit exit
end end
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 begin
channel_threads = number.to_i config.channel_threads = number.to_i
rescue ex rescue ex
puts "THREADS must be integer" puts "THREADS must be integer"
exit exit
end end
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 begin
feed_threads = number.to_i config.feed_threads = number.to_i
rescue ex rescue ex
puts "THREADS must be integer" puts "THREADS must be integer"
exit exit
end end
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 begin
video_threads = number.to_i config.video_threads = number.to_i
rescue ex rescue ex
puts "THREADS must be integer" puts "THREADS must be integer"
exit exit
@ -107,29 +103,31 @@ LOCALES = {
"ru" => load_locale("ru"), "ru" => load_locale("ru"),
} }
crawl_threads.times do config.crawl_threads.times do
spawn do spawn do
crawl_videos(PG_DB, logger) crawl_videos(PG_DB, logger)
end end
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 spawn do
refresh_videos(PG_DB, logger) refresh_videos(PG_DB, logger)
end end
end end
top_videos = [] of Video top_videos = [] of Video
spawn do if config.top_enabled
pull_top_videos(CONFIG, PG_DB) do |videos| spawn do
pull_top_videos(config, PG_DB) do |videos|
top_videos = videos top_videos = videos
sleep 1.minutes sleep 1.minutes
Fiber.yield Fiber.yield
end end
end
end end
popular_videos = [] of ChannelVideo popular_videos = [] of ChannelVideo
@ -231,7 +229,20 @@ get "/" do |env|
end end
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 end
get "/licenses" do |env| get "/licenses" do |env|
@ -367,7 +378,7 @@ get "/watch" do |env|
video.description = replace_links(video.description) video.description = replace_links(video.description)
description = video.short_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 = env.request.query_params
host_params.delete_all("v") host_params.delete_all("v")
@ -467,7 +478,7 @@ get "/embed/:id" do |env|
video.description = replace_links(video.description) video.description = replace_links(video.description)
description = video.short_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 = env.request.query_params
host_params.delete_all("v") host_params.delete_all("v")
@ -553,7 +564,7 @@ get "/opensearch.xml" do |env|
locale = LOCALES[env.get("locale").as(String)]? locale = LOCALES[env.get("locale").as(String)]?
env.response.content_type = "application/opensearchdescription+xml" 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.build(indent: " ", encoding: "UTF-8") do |xml|
xml.element("OpenSearchDescription", xmlns: "http://a9.com/-/spec/opensearch/1.1/") do 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" next env.redirect "/feed/subscriptions"
end end
if !config.login_enabled
error_message = "Login has been disabled by administrator."
next templated "error"
end
referer = get_referer(env, "/feed/subscriptions") referer = get_referer(env, "/feed/subscriptions")
account_type = env.params.query["type"]? account_type = env.params.query["type"]?
@ -716,6 +732,11 @@ post "/login" do |env|
referer = get_referer(env, "/feed/subscriptions") 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"]? email = env.params.body["email"]?
password = env.params.body["password"]? password = env.params.body["password"]?
@ -876,14 +897,14 @@ post "/login" do |env|
host = URI.parse(env.request.headers["Host"]).host host = URI.parse(env.request.headers["Host"]).host
if Kemal.config.ssl || CONFIG.https_only if Kemal.config.ssl || config.https_only
secure = true secure = true
else else
secure = false secure = false
end end
login.cookies.each do |cookie| login.cookies.each do |cookie|
if Kemal.config.ssl || CONFIG.https_only if Kemal.config.ssl || config.https_only
cookie.secure = secure cookie.secure = secure
else else
cookie.secure = secure cookie.secure = secure
@ -912,6 +933,7 @@ post "/login" do |env|
answer = env.params.body["answer"]? answer = env.params.body["answer"]?
text_answer = env.params.body["text_answer"]? text_answer = env.params.body["text_answer"]?
if config.captcha_enabled
if answer if answer
answer = answer.lstrip('0') answer = answer.lstrip('0')
answer = OpenSSL::HMAC.hexdigest(:sha256, HMAC_KEY, answer) answer = OpenSSL::HMAC.hexdigest(:sha256, HMAC_KEY, answer)
@ -961,6 +983,7 @@ post "/login" do |env|
error_message = translate(locale, "CAPTCHA is a required field") error_message = translate(locale, "CAPTCHA is a required field")
next templated "error" next templated "error"
end end
end
action = env.params.body["action"]? action = env.params.body["action"]?
action ||= "signin" action ||= "signin"
@ -992,14 +1015,14 @@ post "/login" do |env|
sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32)) sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32))
PG_DB.exec("INSERT INTO session_ids VALUES ($1, $2, $3)", sid, email, Time.now) 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 secure = true
else else
secure = false secure = false
end end
if CONFIG.domain if config.domain
env.response.cookies["SID"] = HTTP::Cookie.new(name: "SID", domain: ".#{CONFIG.domain}", value: sid, expires: Time.now + 2.years, env.response.cookies["SID"] = HTTP::Cookie.new(name: "SID", domain: ".#{config.domain}", value: sid, expires: Time.now + 2.years,
secure: secure, http_only: true) secure: secure, http_only: true)
else else
env.response.cookies["SID"] = HTTP::Cookie.new(name: "SID", value: sid, expires: Time.now + 2.years, 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) secure: secure, http_only: true)
end end
elsif action == "register" elsif action == "register"
if !config.registration_enabled
error_message = "Registration has been disabled by administrator."
next templated "error"
end
if password.empty? if password.empty?
error_message = translate(locale, "Password cannot be empty") error_message = translate(locale, "Password cannot be empty")
next templated "error" next templated "error"
@ -1049,14 +1077,14 @@ post "/login" do |env|
ucid = ANY ((SELECT subscriptions FROM users WHERE email = E'#{user.email.gsub("'", "\\'")}')::text[]) \ ucid = ANY ((SELECT subscriptions FROM users WHERE email = E'#{user.email.gsub("'", "\\'")}')::text[]) \
ORDER BY published DESC;") ORDER BY published DESC;")
if Kemal.config.ssl || CONFIG.https_only if Kemal.config.ssl || config.https_only
secure = true secure = true
else else
secure = false secure = false
end end
if CONFIG.domain if config.domain
env.response.cookies["SID"] = HTTP::Cookie.new(name: "SID", domain: ".#{CONFIG.domain}", value: sid, expires: Time.now + 2.years, env.response.cookies["SID"] = HTTP::Cookie.new(name: "SID", domain: ".#{config.domain}", value: sid, expires: Time.now + 2.years,
secure: secure, http_only: true) secure: secure, http_only: true)
else else
env.response.cookies["SID"] = HTTP::Cookie.new(name: "SID", value: sid, expires: Time.now + 2.years, 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 = env.params.body["volume"]?.try &.as(String).to_i?
volume ||= DEFAULT_USER_PREFERENCES.volume volume ||= DEFAULT_USER_PREFERENCES.volume
comments_0 = env.params.body["comments_0"]?.try &.as(String) || DEFAULT_USER_PREFERENCES.comments[0] comments = [] of String
comments_1 = env.params.body["comments_1"]?.try &.as(String) || DEFAULT_USER_PREFERENCES.comments[1] 2.times do |i|
comments = [comments_0, comments_1] 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 = [] of String
captions_1 = env.params.body["captions_1"]?.try &.as(String) || DEFAULT_USER_PREFERENCES.captions[1] 3.times do |i|
captions_2 = env.params.body["captions_2"]?.try &.as(String) || DEFAULT_USER_PREFERENCES.captions[2] captions << (env.params.body["captions[#{i}]"]?.try &.as(String) || DEFAULT_USER_PREFERENCES.captions[i])
captions = [captions_0, captions_1, captions_2] end
related_videos = env.params.body["related_videos"]?.try &.as(String) related_videos = env.params.body["related_videos"]?.try &.as(String)
related_videos ||= "off" related_videos ||= "off"
@ -1224,6 +1253,37 @@ post "/preferences" do |env|
if user = env.get? "user" if user = env.get? "user"
user = user.as(User) user = user.as(User)
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
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 else
env.response.cookies["PREFS"] = preferences env.response.cookies["PREFS"] = preferences
end end
@ -1397,7 +1457,7 @@ get "/subscription_manager" do |env|
subscriptions.sort_by! { |channel| channel.author.downcase } subscriptions.sort_by! { |channel| channel.author.downcase }
if action_takeout 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" if format == "json"
env.response.content_type = "application/json" env.response.content_type = "application/json"
@ -1741,7 +1801,11 @@ end
get "/feed/top" do |env| get "/feed/top" do |env|
locale = LOCALES[env.get("locale").as(String)]? locale = LOCALES[env.get("locale").as(String)]?
if config.top_enabled
templated "top" templated "top"
else
env.redirect "/"
end
end end
get "/feed/popular" do |env| get "/feed/popular" do |env|
@ -1984,7 +2048,7 @@ get "/feed/channel/:ucid" do |env|
) )
end 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 path = env.request.path
feed = XML.build(indent: " ", encoding: "UTF-8") do |xml| feed = XML.build(indent: " ", encoding: "UTF-8") do |xml|
@ -2118,7 +2182,7 @@ get "/feed/private" do |env|
videos = videos[0..max_results] videos = videos[0..max_results]
end 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 path = env.request.path
query = env.request.query.not_nil! query = env.request.query.not_nil!
@ -2173,7 +2237,7 @@ get "/feed/playlist/:plid" do |env|
plid = env.params.url["plid"] 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 path = env.request.path
client = make_client(YT_URL) client = make_client(YT_URL)
@ -2487,7 +2551,7 @@ get "/api/v1/insights/:id" do |env|
env.response.content_type = "application/json" env.response.content_type = "application/json"
error_message = {"error" => "YouTube has removed publicly-available analytics."}.to_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) client = make_client(YT_URL)
headers = HTTP::Headers.new headers = HTTP::Headers.new
@ -2653,7 +2717,7 @@ get "/api/v1/videos/:id" do |env|
end end
if video.player_response["streamingData"]?.try &.["hlsManifestUrl"]? 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 = env.request.query_params
host_params.delete_all("v") host_params.delete_all("v")
@ -2871,6 +2935,11 @@ get "/api/v1/top" do |env|
env.response.content_type = "application/json" 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| videos = JSON.build do |json|
json.array do json.array do
top_videos.each do |video| 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.content_type = "application/x-mpegURL"
env.response.headers.add("Access-Control-Allow-Origin", "*") 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 = manifest.body
manifest.gsub("https://www.youtube.com", host_url) 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 halt env, status_code: manifest.status_code
end 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.body.gsub("https://www.youtube.com", host_url)
manifest = manifest.gsub(/https:\/\/r\d---.{11}\.c\.youtube\.com/, host_url) manifest = manifest.gsub(/https:\/\/r\d---.{11}\.c\.youtube\.com/, host_url)

View file

@ -1,9 +1,9 @@
class Config class Config
YAML.mapping({ 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) 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) 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 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 db: NamedTuple( # Database configuration
user: String, user: String,
password: String, password: String,
@ -11,11 +11,18 @@ user: String,
port: Int32, port: Int32,
dbname: String, dbname: String,
), ),
dl_api_key: String?, # DetectLanguage API Key (used to filter non-English results from "top" page), mostly non-functional 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:// 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 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 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 end

View file

@ -133,7 +133,7 @@ def refresh_feeds(db, logger, max_threads = 1)
rescue ex rescue ex
# Create view if it doesn't exist # Create view if it doesn't exist
if ex.message.try &.ends_with? "does not 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 \ SELECT * FROM channel_videos WHERE \
ucid = ANY ((SELECT subscriptions FROM users WHERE email = E'#{email.gsub("'", "\\'")}')::text[]) \ ucid = ANY ((SELECT subscriptions FROM users WHERE email = E'#{email.gsub("'", "\\'")}')::text[]) \
ORDER BY published DESC;") ORDER BY published DESC;")
@ -193,11 +193,11 @@ end
def pull_popular_videos(db) def pull_popular_videos(db)
loop do 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 \ (SELECT UNNEST(subscriptions) AS channel FROM users) AS d \
GROUP BY channel ORDER BY COUNT(channel) DESC LIMIT 40", as: String) 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)}) \ channel_videos WHERE ucid IN (#{arg_array(subscriptions)}) \
ORDER BY ucid, published DESC", subscriptions, as: ChannelVideo).sort_by { |video| video.published }.reverse ORDER BY ucid, published DESC", subscriptions, as: ChannelVideo).sort_by { |video| video.published }.reverse

View file

@ -143,7 +143,7 @@ def get_user(sid, headers, db, refresh = true)
begin begin
view_name = "subscriptions_#{sha256(user.email)[0..7]}" 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 \ SELECT * FROM channel_videos WHERE \
ucid = ANY ((SELECT subscriptions FROM users WHERE email = E'#{user.email.gsub("'", "\\'")}')::text[]) \ ucid = ANY ((SELECT subscriptions FROM users WHERE email = E'#{user.email.gsub("'", "\\'")}')::text[]) \
ORDER BY published DESC;") ORDER BY published DESC;")
@ -165,7 +165,7 @@ def get_user(sid, headers, db, refresh = true)
begin begin
view_name = "subscriptions_#{sha256(user.email)[0..7]}" 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 \ SELECT * FROM channel_videos WHERE \
ucid = ANY ((SELECT subscriptions FROM users WHERE email = E'#{user.email.gsub("'", "\\'")}')::text[]) \ ucid = ANY ((SELECT subscriptions FROM users WHERE email = E'#{user.email.gsub("'", "\\'")}')::text[]) \
ORDER BY published DESC;") ORDER BY published DESC;")
@ -247,7 +247,7 @@ def validate_response(challenge, token, user_id, operation, key, db, locale)
raise translate(locale, "Invalid challenge") raise translate(locale, "Invalid challenge")
end end
challenge = OpenSSL::HMAC.digest(:sha256, HMAC_KEY, challenge) challenge = OpenSSL::HMAC.digest(:sha256, key, challenge)
challenge = Base64.urlsafe_encode(challenge) challenge = Base64.urlsafe_encode(challenge)
if db.query_one?("SELECT EXISTS (SELECT true FROM nonces WHERE nonce = $1)", nonce, as: Bool) if db.query_one?("SELECT EXISTS (SELECT true FROM nonces WHERE nonce = $1)", nonce, as: Bool)

View file

@ -2,12 +2,12 @@
<div class="pure-u-1 pure-u-md-1-4"></div> <div class="pure-u-1 pure-u-md-1-4"></div>
<div class="pure-u-1 pure-u-md-1-2"> <div class="pure-u-1 pure-u-md-1-2">
<div class="pure-g"> <div class="pure-g">
<% feeds = ["Popular", "Top", "Trending"] %> <% feed_menu = config.feed_menu.dup %>
<% if env.get? "user" %> <% if !env.get?("user") %>
<% feeds << "Subscriptions" %> <% feed_menu.reject! {|feed| feed == "Subscriptions"} %>
<% end %> <% end %>
<% feeds.each do |feed| %> <% feed_menu.each do |feed| %>
<div class="pure-u-1-2 pure-u-md-1-<%= feeds.size %>"> <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"> <a href="/feed/<%= feed.downcase %>" style="text-align:center;" class="pure-menu-heading">
<%= translate(locale, feed) %> <%= translate(locale, feed) %>
</a> </a>

View file

@ -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>

View file

@ -28,6 +28,7 @@
<label for="password"><%= translate(locale, "Password:") %></label> <label for="password"><%= translate(locale, "Password:") %></label>
<input required class="pure-input-1" name="password" type="password" placeholder="Password"> <input required class="pure-input-1" name="password" type="password" placeholder="Password">
<% if config.captcha_enabled %>
<% if captcha_type == "image" %> <% if captcha_type == "image" %>
<img style="width:100%" src='<%= captcha.not_nil![:image] %>'/> <img style="width:100%" src='<%= captcha.not_nil![:image] %>'/>
<input type="hidden" name="token" value="<%= captcha.not_nil![:token] %>"> <input type="hidden" name="token" value="<%= captcha.not_nil![:token] %>">
@ -54,9 +55,12 @@
</a> </a>
</label> </label>
<% end %> <% end %>
<% end %>
<button type="submit" name="action" value="signin" class="pure-button pure-button-primary"><%= translate(locale, "Sign In") %></button> <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> <button type="submit" name="action" value="register" class="pure-button pure-button-primary"><%= translate(locale, "Register") %></button>
<% end %>
</fieldset> </fieldset>
</form> </form>
<% elsif account_type == "google" %> <% elsif account_type == "google" %>

View file

@ -1,6 +1,6 @@
<% content_for "header" do %> <% content_for "header" do %>
<meta name="description" content="<%= translate(locale, "An alternative front-end to YouTube") %>"> <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 %> <% end %>
<%= rendered "components/feed_menu" %> <%= rendered "components/feed_menu" %>

View file

@ -58,45 +58,25 @@ function update_value(element) {
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="comments_0"><%= translate(locale, "Default comments: ") %></label> <label for="comments[0]"><%= translate(locale, "Default comments: ") %></label>
<select name="comments_0" id="comments_0"> <% preferences.comments.each_with_index do |comments, index| %>
<select name="comments[<%= index %>]" id="comments[<%= index %>]">
<% {"", "youtube", "reddit"}.each do |option| %> <% {"", "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 %> <% end %>
</select> </select>
<% end %>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="comments_1"><%= translate(locale, "Fallback comments: ") %></label> <label for="captions[0]"><%= translate(locale, "Default captions: ") %></label>
<select name="comments_1" id="comments_1"> <% preferences.captions.each_with_index do |caption, index| %>
<% {"", "youtube", "reddit"}.each do |option| %> <select class="pure-u-1-6" name="captions[<%= index %>]" id="captions[<%= index %>]">
<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| %> <% CAPTION_LANGUAGES.each do |option| %>
<option value="<%= option %>" <% if preferences.captions[0] == option %> selected <% end %>><%= translate(locale, option) %></option> <option value="<%= option %>" <% if preferences.captions[index] == option %> selected <% end %>><%= translate(locale, option) %></option>
<% end %> <% end %>
</select> </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">
<% CAPTION_LANGUAGES.each do |option| %>
<option value="<%= option %>" <% if preferences.captions[1] == option %> selected <% end %>><%= translate(locale, option) %></option>
<% end %> <% 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>
<div class="pure-control-group"> <div class="pure-control-group">
@ -167,6 +147,50 @@ function update_value(element) {
</div> </div>
<% end %> <% 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" %> <% if env.get? "user" %>
<legend><%= translate(locale, "Data preferences") %></legend> <legend><%= translate(locale, "Data preferences") %></legend>

View file

@ -89,12 +89,14 @@
<i class="icon ion-ios-cog"></i> <i class="icon ion-ios-cog"></i>
</a> </a>
</div> </div>
<% if config.login_enabled %>
<div class="pure-u-1-3"> <div class="pure-u-1-3">
<a href="/login?referer=<%= env.get?("current_page") %>" class="pure-menu-heading"> <a href="/login?referer=<%= env.get?("current_page") %>" class="pure-menu-heading">
<%= translate(locale, "Login") %> <%= translate(locale, "Login") %>
</a> </a>
</div> </div>
<% end %> <% end %>
<% end %>
</div> </div>
</div> </div>
<%= content %> <%= content %>

View file

@ -1,6 +1,6 @@
<% content_for "header" do %> <% content_for "header" do %>
<meta name="description" content="<%= translate(locale, "An alternative front-end to YouTube") %>"> <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 %> <% end %>
<%= rendered "components/feed_menu" %> <%= rendered "components/feed_menu" %>

View file

@ -1,6 +1,6 @@
<% content_for "header" do %> <% content_for "header" do %>
<meta name="description" content="<%= translate(locale, "An alternative front-end to YouTube") %>"> <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 %> <% end %>
<%= rendered "components/feed_menu" %> <%= rendered "components/feed_menu" %>