From 276bf09238103aaabe64be09b9ac10f976510ff7 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Wed, 20 Nov 2019 12:03:52 -0500 Subject: [PATCH 0001/2930] Skip preferences for assets --- src/invidious.cr | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/invidious.cr b/src/invidious.cr index a766a11c..559214ac 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -255,6 +255,17 @@ before_all do |env| env.response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains; preload" end + next if { + "/sb/", + "/vi/", + "/s_p/", + "/yts/", + "/ggpht/", + "/api/manifest/", + "/videoplayback", + "/latest_version", + }.any? { |r| env.request.resource.starts_with? r } + begin preferences = Preferences.from_json(env.request.cookies["PREFS"]?.try &.value || "{}") rescue From 48cbe45a9d3212a6f32551741213b028700d59b2 Mon Sep 17 00:00:00 2001 From: edumoreira1506 Date: Wed, 20 Nov 2019 15:59:07 -0300 Subject: [PATCH 0002/2930] Add Previous/Next page buttons at the top of the page --- src/invidious/views/search.ecr | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/invidious/views/search.ecr b/src/invidious/views/search.ecr index d084bd31..bc13b7ea 100644 --- a/src/invidious/views/search.ecr +++ b/src/invidious/views/search.ecr @@ -2,6 +2,24 @@ <%= search_query.not_nil!.size > 30 ? HTML.escape(query.not_nil![0,30].rstrip(".") + "...") : HTML.escape(query.not_nil!) %> - Invidious <% end %> +
+
+ <% if page > 1 %> + + <%= translate(locale, "Previous page") %> + + <% end %> +
+
+
+ <% if count >= 20 %> + + <%= translate(locale, "Next page") %> + + <% end %> +
+
+
<% videos.each_slice(4) do |slice| %> <% slice.each do |item| %> From 0e3a48ff76aa0d5ea5f3f135c01ddde02b217dff Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Sun, 24 Nov 2019 13:41:47 -0500 Subject: [PATCH 0003/2930] Update QUICPool --- shard.yml | 2 +- src/invidious.cr | 40 +++++++------- src/invidious/helpers/jobs.cr | 4 +- src/invidious/helpers/utils.cr | 98 ++++++++-------------------------- 4 files changed, 44 insertions(+), 100 deletions(-) diff --git a/shard.yml b/shard.yml index 96cdb1fc..c221e681 100644 --- a/shard.yml +++ b/shard.yml @@ -26,7 +26,7 @@ dependencies: version: ~> 0.1.2 lsquic: github: omarroth/lsquic.cr - version: ~> 0.1.3 + version: ~> 0.1.4 crystal: 0.31.1 diff --git a/src/invidious.cr b/src/invidious.cr index 559214ac..147fe935 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -53,8 +53,8 @@ CHARS_SAFE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz012345 TEST_IDS = {"AgbeGFYluEA", "BaW_jenozKc", "a9LDPn-MO4I", "ddFvjfvPnqk", "iqKdEhx-dD4"} MAX_ITEMS_PER_PAGE = 1500 -REQUEST_HEADERS_WHITELIST = {"Accept", "Accept-Encoding", "Cache-Control", "Connection", "Content-Length", "If-None-Match", "Range"} -RESPONSE_HEADERS_BLACKLIST = {"Access-Control-Allow-Origin", "Alt-Svc", "Server"} +REQUEST_HEADERS_WHITELIST = {"accept", "accept-encoding", "cache-control", "content-length", "if-none-match", "range"} +RESPONSE_HEADERS_BLACKLIST = {"access-control-allow-origin", "alt-svc", "server"} HTTP_CHUNK_SIZE = 10485760 # ~10MB CURRENT_BRANCH = {{ "#{`git branch | sed -n '/\* /s///p'`.strip}" }} @@ -95,7 +95,7 @@ LOCALES = { } YT_POOL = QUICPool.new(YT_URL, capacity: CONFIG.pool_size, timeout: 0.05) -YT_IMG_POOL = HTTPPool.new(YT_IMG_URL, capacity: CONFIG.pool_size, timeout: 0.05) +YT_IMG_POOL = QUICPool.new(YT_IMG_URL, capacity: CONFIG.pool_size, timeout: 0.05) config = CONFIG logger = Invidious::LogHandler.new @@ -1448,7 +1448,7 @@ post "/login" do |env| # See https://github.com/ytdl-org/youtube-dl/blob/2019.04.07/youtube_dl/extractor/youtube.py#L82 # TODO: Convert to QUIC begin - client = make_client(LOGIN_URL) + client = QUIC::Client.new(LOGIN_URL) headers = HTTP::Headers.new login_page = client.get("/ServiceLogin") @@ -1471,7 +1471,6 @@ post "/login" do |env| headers["Content-Type"] = "application/x-www-form-urlencoded;charset=utf-8" headers["Google-Accounts-XSRF"] = "1" - headers["User-Agent"] = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36" response = client.post("/_/signin/sl/lookup", headers, login_req(lookup_req)) lookup_results = JSON.parse(response.body[5..-1]) @@ -1645,28 +1644,31 @@ post "/login" do |env| traceback << "Logging in..." - location = challenge_results[0][-1][2].to_s + location = URI.parse(challenge_results[0][-1][2].to_s) cookies = HTTP::Cookies.from_headers(headers) + headers.delete("Content-Type") + headers.delete("Google-Accounts-XSRF") + loop do - if !location || location.includes? "/ManageAccount" + if !location || location.path == "/ManageAccount" break end # Occasionally there will be a second page after login confirming # the user's phone number ("/b/0/SmsAuthInterstitial"), which we currently don't handle. - if location.includes? "/b/0/SmsAuthInterstitial" + if location.path.starts_with? "/b/0/SmsAuthInterstitial" traceback << "Unhandled dialog /b/0/SmsAuthInterstitial." end - login = client.get(location, headers) - headers = login.cookies.add_request_headers(headers) + login = client.get(location.full_path, headers) - cookies = HTTP::Cookies.from_headers(headers) - location = login.headers["Location"]? + headers = login.cookies.add_request_headers(headers) + location = login.headers["Location"]?.try { |u| URI.parse(u) } end + cookies = HTTP::Cookies.from_headers(headers) sid = cookies["SID"]?.try &.value if !sid raise "Couldn't get SID." @@ -5534,7 +5536,7 @@ get "/videoplayback" do |env| client = make_client(URI.parse(host), region) client.get(url, headers) do |response| response.headers.each do |key, value| - if !RESPONSE_HEADERS_BLACKLIST.includes?(key) + if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase) env.response.headers[key] = value end end @@ -5602,7 +5604,7 @@ get "/videoplayback" do |env| end response.headers.each do |key, value| - if !RESPONSE_HEADERS_BLACKLIST.includes?(key) && key != "Content-Range" + if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase) && key.downcase != "content-range" env.response.headers[key] = value end end @@ -5666,7 +5668,7 @@ get "/ggpht/*" do |env| client.get(url, headers) do |response| env.response.status_code = response.status_code response.headers.each do |key, value| - if !RESPONSE_HEADERS_BLACKLIST.includes? key + if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase) env.response.headers[key] = value end end @@ -5716,7 +5718,7 @@ get "/sb/:id/:storyboard/:index" do |env| client.get(url, headers) do |response| env.response.status_code = response.status_code response.headers.each do |key, value| - if !RESPONSE_HEADERS_BLACKLIST.includes? key + if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase) env.response.headers[key] = value end end @@ -5753,7 +5755,7 @@ get "/s_p/:id/:name" do |env| client.get(url, headers) do |response| env.response.status_code = response.status_code response.headers.each do |key, value| - if !RESPONSE_HEADERS_BLACKLIST.includes? key + if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase) env.response.headers[key] = value end end @@ -5783,7 +5785,7 @@ get "/yts/img/:name" do |env| YT_POOL.client &.get(env.request.resource, headers) do |response| env.response.status_code = response.status_code response.headers.each do |key, value| - if !RESPONSE_HEADERS_BLACKLIST.includes? key + if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase) env.response.headers[key] = value end end @@ -5826,7 +5828,7 @@ get "/vi/:id/:name" do |env| YT_IMG_POOL.client &.get(url, headers) do |response| env.response.status_code = response.status_code response.headers.each do |key, value| - if !RESPONSE_HEADERS_BLACKLIST.includes? key + if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase) env.response.headers[key] = value end end diff --git a/src/invidious/helpers/jobs.cr b/src/invidious/helpers/jobs.cr index d304a27e..5838b5b3 100644 --- a/src/invidious/helpers/jobs.cr +++ b/src/invidious/helpers/jobs.cr @@ -127,8 +127,6 @@ def subscribe_to_feeds(db, logger, key, config) end max_channel = Channel(Int32).new - client_pool = HTTPPool.new(PUBSUB_URL, capacity: max_threads, timeout: 0.05) - spawn do max_threads = max_channel.receive active_threads = 0 @@ -149,7 +147,7 @@ def subscribe_to_feeds(db, logger, key, config) spawn do begin - response = subscribe_pubsub(ucid, key, config, client_pool) + response = subscribe_pubsub(ucid, key, config) if response.status_code >= 400 logger.puts("#{ucid} : #{response.body}") diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index 8c5ce7a0..53c18dd5 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -11,11 +11,11 @@ def add_yt_headers(request) request.headers["cookie"] = "#{(CONFIG.cookies.map { |c| "#{c.name}=#{c.value}" }).join("; ")}; #{request.headers["cookie"]?}" end -struct HTTPPool +struct QUICPool property! url : URI property! capacity : Int32 property! timeout : Float64 - property pool : ConnectionPool(HTTPClient) + property pool : ConnectionPool(QUIC::Client) def initialize(url : URI, @capacity = 5, @timeout = 5.0) @url = url @@ -23,91 +23,35 @@ struct HTTPPool end def client(region = nil, &block) - conn = pool.checkout - - begin - if region - PROXY_LIST[region]?.try &.sample(40).each do |proxy| - begin - proxy = HTTPProxy.new(proxy_host: proxy[:ip], proxy_port: proxy[:port]) - conn.set_proxy(proxy) - break - rescue ex - end - end - end - + if region + conn = make_client(url, region) response = yield conn - - if region - conn.unset_proxy + else + conn = pool.checkout + begin + response = yield conn + rescue ex + conn.destroy_engine + conn = QUIC::Client.new(url) + conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com" + response = yield conn + ensure + pool.checkin(conn) end - - response - rescue ex - conn = HTTPClient.new(url) - conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com" - conn.family = (url.host == "www.youtube.com" || url.host == "suggestqueries.google.com") ? CONFIG.force_resolve : Socket::Family::UNSPEC - conn.read_timeout = 10.seconds - conn.connect_timeout = 10.seconds - yield conn - ensure - pool.checkin(conn) end + + response end private def build_pool - ConnectionPool(HTTPClient).new(capacity: capacity, timeout: timeout) do - client = HTTPClient.new(url) + ConnectionPool(QUIC::Client).new(capacity: capacity, timeout: timeout) do + client = QUIC::Client.new(url) client.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com" - client.family = (url.host == "www.youtube.com" || url.host == "suggestqueries.google.com") ? CONFIG.force_resolve : Socket::Family::UNSPEC - client.read_timeout = 10.seconds - client.connect_timeout = 10.seconds client end end end -struct QUICPool - property! url : URI - property! capacity : Int32 - property! timeout : Float64 - - def initialize(url : URI, @capacity = 5, @timeout = 5.0) - @url = url - end - - def client(region = nil, &block) - begin - if region - client = HTTPClient.new(url) - client.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com" - client.read_timeout = 10.seconds - client.connect_timeout = 10.seconds - - PROXY_LIST[region]?.try &.sample(40).each do |proxy| - begin - proxy = HTTPProxy.new(proxy_host: proxy[:ip], proxy_port: proxy[:port]) - client.set_proxy(proxy) - break - rescue ex - end - end - - yield client - else - conn = QUIC::Client.new(url) - conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com" - yield conn - end - rescue ex - conn = QUIC::Client.new(url) - conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com" - yield conn - end - end -end - # See http://www.evanmiller.org/how-not-to-sort-by-average-rating.html def ci_lower_bound(pos, n) if n == 0 @@ -419,7 +363,7 @@ def sha256(text) return digest.hexdigest end -def subscribe_pubsub(topic, key, config, client_pool) +def subscribe_pubsub(topic, key, config) case topic when .match(/^UC[A-Za-z0-9_-]{22}$/) topic = "channel_id=#{topic}" @@ -446,7 +390,7 @@ def subscribe_pubsub(topic, key, config, client_pool) "hub.secret" => key.to_s, } - return client_pool.client &.post("/subscribe", form: body) + return make_client(PUBSUB_URL).post("/subscribe", form: body) end def parse_range(range) From cf97dd9fcd7e561ca7a9732fe5256d0c51811beb Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Sun, 24 Nov 2019 14:00:42 -0500 Subject: [PATCH 0004/2930] Bump dependencies --- shard.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/shard.yml b/shard.yml index c221e681..0cff3612 100644 --- a/shard.yml +++ b/shard.yml @@ -26,7 +26,8 @@ dependencies: version: ~> 0.1.2 lsquic: github: omarroth/lsquic.cr - version: ~> 0.1.4 + branch: 15a67d48a09023d7b6fabdc9df1aacf77e39f3ff + # version: ~> 0.1.4 crystal: 0.31.1 From 3fea1976c835ecd181bbf4c2ca02ee88d61370a1 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Sun, 24 Nov 2019 15:26:19 -0500 Subject: [PATCH 0005/2930] Update dependencies --- shard.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/shard.yml b/shard.yml index 0cff3612..6287ca63 100644 --- a/shard.yml +++ b/shard.yml @@ -26,8 +26,7 @@ dependencies: version: ~> 0.1.2 lsquic: github: omarroth/lsquic.cr - branch: 15a67d48a09023d7b6fabdc9df1aacf77e39f3ff - # version: ~> 0.1.4 + version: ~> 0.1.5 crystal: 0.31.1 From ae24360c027efc7eacfe5427f22b83c2df2815bf Mon Sep 17 00:00:00 2001 From: tleydxdy Date: Tue, 26 Nov 2019 18:20:23 -0500 Subject: [PATCH 0006/2930] Proper fix for docker build return to static linking --- docker/Dockerfile | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 0defb92b..1bb4d11f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,28 +1,32 @@ -FROM alpine:edge +FROM alpine:edge AS builder RUN apk add --no-cache crystal shards libc-dev \ - yaml-dev libxml2-dev sqlite-dev zlib-dev curl && \ - curl -Lo /etc/apk/keys/omarroth.rsa.pub https://github.com/omarroth/boringssl-alpine/releases/download/1.1.0-r0/omarroth.rsa.pub && \ - curl -Lo boringssl-dev.apk https://github.com/omarroth/boringssl-alpine/releases/download/1.1.0-r0/boringssl-dev-1.1.0-r0.apk && \ - curl -Lo lsquic.apk https://github.com/omarroth/lsquic-alpine/releases/download/2.6.3-r0/lsquic-2.6.3-r0.apk && \ - apk update && \ - apk add boringssl-dev.apk lsquic.apk && \ - rm -rf /var/cache/apk/* boringssl-dev.apk lsquic.apk + yaml-dev libxml2-dev sqlite-dev zlib-dev openssl-dev \ + sqlite-static zlib-static openssl-libs-static WORKDIR /invidious COPY ./shard.yml ./shard.yml RUN shards update && shards install -RUN cp /usr/lib/libcrypto.a ./lib/lsquic/src/lsquic/ext/libcrypto.a && \ - cp /usr/lib/libssl.a ./lib/lsquic/src/lsquic/ext/libssl.a && \ - cp /usr/lib/liblsquic.a ./lib/lsquic/src/lsquic/ext/liblsquic.a +RUN apk add --no-cache curl && \ + curl -Lo /etc/apk/keys/omarroth.rsa.pub https://github.com/omarroth/boringssl-alpine/releases/download/1.1.0-r0/omarroth.rsa.pub && \ + curl -Lo boringssl-dev.apk https://github.com/omarroth/boringssl-alpine/releases/download/1.1.0-r0/boringssl-dev-1.1.0-r0.apk && \ + curl -Lo lsquic.apk https://github.com/omarroth/lsquic-alpine/releases/download/2.6.3-r0/lsquic-2.6.3-r0.apk && \ + tar -xf boringssl-dev.apk && \ + tar -xf lsquic.apk +RUN mv ./usr/lib/libcrypto.a ./lib/lsquic/src/lsquic/ext/libcrypto.a && \ + mv ./usr/lib/libssl.a ./lib/lsquic/src/lsquic/ext/libssl.a && \ + mv ./usr/lib/liblsquic.a ./lib/lsquic/src/lsquic/ext/liblsquic.a COPY ./src/ ./src/ # TODO: .git folder is required for building – this is destructive. # See definition of CURRENT_BRANCH, CURRENT_COMMIT and CURRENT_VERSION. COPY ./.git/ ./.git/ -RUN crystal build --release --warnings all --error-on-warnings \ - # TODO: Remove next line, see https://github.com/crystal-lang/crystal/issues/7946 +RUN crystal build ./src/invidious.cr \ + --release --static --warnings all --error-on-warnings \ +# TODO: Remove next line, see https://github.com/crystal-lang/crystal/issues/7946 -Dmusl \ - ./src/invidious.cr + --link-flags "-lxml2 -llzma" +FROM alpine:latest RUN apk add --no-cache librsvg ttf-opensans +WORKDIR /invidious RUN addgroup -g 1000 -S invidious && \ adduser -u 1000 -S invidious -G invidious COPY ./assets/ ./assets/ @@ -30,5 +34,6 @@ COPY ./config/config.yml ./config/config.yml COPY ./config/sql/ ./config/sql/ COPY ./locales/ ./locales/ RUN sed -i 's/host: \(127.0.0.1\|localhost\)/host: postgres/' config/config.yml +COPY --from=builder /invidious/invidious . USER invidious CMD [ "/invidious/invidious" ] From a017574f74665edc9645b9e1c0414e0a34e13f41 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Thu, 28 Nov 2019 08:19:28 -0600 Subject: [PATCH 0007/2930] Add support for force_resolve to QUIC client --- shard.yml | 2 +- src/invidious.cr | 9 +++++---- src/invidious/helpers/utils.cr | 12 ++++++++---- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/shard.yml b/shard.yml index 6287ca63..d5901a78 100644 --- a/shard.yml +++ b/shard.yml @@ -26,7 +26,7 @@ dependencies: version: ~> 0.1.2 lsquic: github: omarroth/lsquic.cr - version: ~> 0.1.5 + version: ~> 0.1.7 crystal: 0.31.1 diff --git a/src/invidious.cr b/src/invidious.cr index 147fe935..59590f22 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -4523,10 +4523,11 @@ get "/api/v1/search/suggestions" do |env| query ||= "" begin - response = QUIC::Client.get( - "https://suggestqueries.google.com/complete/search?hl=en&gl=#{region}&client=youtube&ds=yt&q=#{URI.encode_www_form(query)}&callback=suggestCallback" - ).body - + client = QUIC::Client.new("suggestqueries.google.com") + client.family = CONFIG.force_resolve || Socket::Family::INET + client.family = Socket::Family::INET if client.family == Socket::Family::UNSPEC + response = client.get("/complete/search?hl=en&gl=#{region}&client=youtube&ds=yt&q=#{URI.encode_www_form(query)}&callback=suggestCallback").body + body = response[35..-2] body = JSON.parse(body).as_a suggestions = body[1].as_a[0..-2] diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index 53c18dd5..6fcfa8d2 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -31,8 +31,10 @@ struct QUICPool begin response = yield conn rescue ex - conn.destroy_engine + conn.close conn = QUIC::Client.new(url) + conn.family = (url.host == "www.youtube.com") ? CONFIG.force_resolve : Socket::Family::INET + conn.family = Socket::Family::INET if conn.family == Socket::Family::UNSPEC conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com" response = yield conn ensure @@ -45,9 +47,11 @@ struct QUICPool private def build_pool ConnectionPool(QUIC::Client).new(capacity: capacity, timeout: timeout) do - client = QUIC::Client.new(url) - client.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com" - client + conn = QUIC::Client.new(url) + conn.family = (url.host == "www.youtube.com") ? CONFIG.force_resolve : Socket::Family::INET + conn.family = Socket::Family::INET if conn.family == Socket::Family::UNSPEC + conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com" + conn end end end From 04d56420d1e0aecf85dd7d4f51d3339b88b61caa Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Thu, 28 Nov 2019 08:20:44 -0600 Subject: [PATCH 0008/2930] Run 'crystal tool format' --- src/invidious.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious.cr b/src/invidious.cr index 59590f22..6499d569 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -4527,7 +4527,7 @@ get "/api/v1/search/suggestions" do |env| client.family = CONFIG.force_resolve || Socket::Family::INET client.family = Socket::Family::INET if client.family == Socket::Family::UNSPEC response = client.get("/complete/search?hl=en&gl=#{region}&client=youtube&ds=yt&q=#{URI.encode_www_form(query)}&callback=suggestCallback").body - + body = response[35..-2] body = JSON.parse(body).as_a suggestions = body[1].as_a[0..-2] From fc7f48b7db2fd7da53a287402b2053c637ff32b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vlad=20Crang=C4=83?= <44527421+vcvlad@users.noreply.github.com> Date: Thu, 28 Nov 2019 15:09:41 +0000 Subject: [PATCH 0009/2930] Create ro.json --- locales/ro.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 locales/ro.json diff --git a/locales/ro.json b/locales/ro.json new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/locales/ro.json @@ -0,0 +1 @@ + From f3e0c5d6539c88d3282702189e5477a24158c624 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vlad=20Crang=C4=83?= <44527421+vcvlad@users.noreply.github.com> Date: Thu, 28 Nov 2019 17:16:46 +0000 Subject: [PATCH 0010/2930] Update ro.json Invidious translated from English into Romanian. --- locales/ro.json | 336 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 336 insertions(+) diff --git a/locales/ro.json b/locales/ro.json index 8b137891..57c76a83 100644 --- a/locales/ro.json +++ b/locales/ro.json @@ -1 +1,337 @@ +{ + "`x` subscribers": "`x` abonați", + "`x` videos": "`x` videoclipuri", + "`x` playlists": "`x` liste de redare", + "LIVE": "ÎN DIRECT", + "Shared `x` ago": "Adăugat acum `x`", + "Unsubscribe": "Dezabonați-vă", + "Subscribe": "Abonați-vă", + "View channel on YouTube": "Vedeți canalul pe YouTube", + "View playlist on YouTube": "Vedeți lista de redare pe YouTube", + "newest": "Data adăugării (cea mai recentă)", + "oldest": "Data adăugării (cea mai veche)", + "popular": "Cele mai populare", + "last": "Ultimele", + "Next page": "Pagina următoare", + "Previous page": "Pagina precedentă", + "Clear watch history?": "Doriți să ștergeți istoricul?", + "New password": "Parola nouă", + "New passwords must match": "Câmpurile \"Parolă nouă\" trebuie să fie identice", + "Cannot change password for Google accounts": "Parola pentru un cont Google nu poate fi schimbată de pe Invidious", + "Authorize token?": "Autorizați token-ul?", + "Authorize token for `x`?": "Autorizați token-ul pentru `x` ?", + "Yes": "Da", + "No": "Nu", + "Import and Export Data": "Importați și Exportați Datele", + "Import": "Importați", + "Import Invidious data": "Importați Datele de pe Invidious", + "Import YouTube subscriptions": "Importați abonamentele de pe YouTube", + "Import FreeTube subscriptions (.db)": "Importați abonamentele de pe FreeTube (.db)", + "Import NewPipe subscriptions (.json)": "Importați abonamentele de pe NewPipe (.json)", + "Import NewPipe data (.zip)": "Importați datele de pe NewPipe (.zip)", + "Export": "Exportați", + "Export subscriptions as OPML": "Exportați abonamentele în format OPML", + "Export subscriptions as OPML (for NewPipe & FreeTube)": "Exportați abonamentele în format OPML (pentru NewPipe și FreeTube)", + "Export data as JSON": "Exportați datele în format JSON", + "Delete account?": "Sunteți siguri că doriți să vă ștergeți contul?", + "History": "Istoric", + "An alternative front-end to YouTube": "O alternativă front-end pentru YouTube", + "JavaScript license information": "Informații despre licențele JavaScript", + "source": "sursă", + "Log in": "Conectați-vă", + "Log in/register": "Conectați-vă/Creați-vă un cont", + "Log in with Google": "Conectați-vă cu Google", + "User ID": "ID Utilizator", + "Password": "Parolă", + "Time (h:mm:ss):": "Ora (h:mm:ss) :", + "Text CAPTCHA": "Text CAPTCHA", + "Image CAPTCHA": "Imagine CAPTCHA", + "Sign In": "Conectați-vă", + "Register": "Înregistrați-vă", + "E-mail": "E-mail", + "Google verification code": "Cod de verificare Google", + "Preferences": "Preferințe", + "Player preferences": "Setări de redare", + "Always loop: ": "Reluați videoclipul la nesfârșit: ", + "Autoplay: ": "Porniți videoclipurile automat: ", + "Play next by default: ": "Vizionați următoarele videoclipuri în mod implicit: ", + "Autoplay next video: ": "Porniți următorul videoclip automat: ", + "Listen by default: ": "Numai audio: ", + "Proxy videos: " : "Redați videoclipurile printr-un proxy: ", + "Default speed: ": "Viteza de redare implicită: ", + "Preferred video quality: ": "Calitatea videoclipurilor: ", + "Player volume: ": "Volumul videoclipurilor: ", + "Default comments: ": "Sursa comentariilor: ", + "youtube": "YouTube", + "reddit": "Reddit", + "Default captions: ": "Subtitrări implicite: ", + "Fallback captions: ": "Subtitrări alternative: ", + "Show related videos: ": "Afișați videoclipurile asemănătoare: ", + "Show annotations by default: ": "Afișați adnotările în mod implicit: ", + "Visual preferences": "Preferințele site-ului", + "Player style: ": "Stilul player-ului : ", + "Dark mode: ": "Modul întunecat : ", + "Theme: ": "Tema : ", + "dark": "întunecat", + "light": "luminos", + "Thin mode: ": "Mod lejer: ", + "Subscription preferences": "Preferințele paginii de abonamente", + "Show annotations by default for subscribed channels: ": "Afișați adnotările în mod implicit pentru canalele la care v-ați abonat: ", + "Redirect homepage to feed: ": "Redirecționați pagina principală la pagina de abonamente: ", + "Number of videos shown in feed: ": "Numărul de videoclipuri afișate pe pagina de abonamente: ", + "Sort videos by: ": "Sortați videoclipurile în funcție de: ", + "published": "data publicării", + "published - reverse": "data publicării - inversată", + "alphabetically": "în ordine alfabetică", + "alphabetically - reverse": "în ordine alfabetică - inversată", + "channel name": "numele canalului", + "channel name - reverse": "numele canalului - inversat", + "Only show latest video from channel: ": "Afișați numai cel mai recent videoclip publicat de canalele la care v-ați abonat: ", + "Only show latest unwatched video from channel: ": "Afișați numai cel mai recent videoclip nevizionat publicat de canalele la care v-ați abonat: ", + "Only show unwatched: ": "Afișați numai videoclipurile nevizionate: ", + "Only show notifications (if there are any): ": "Afișați numai notificările (dacă există): ", + "Enable web notifications": "Activați notificările web", + "`x` uploaded a video": "`x` a publicat un videoclip", + "`x` is live": "`x` este în direct", + "Data preferences": "Preferințe legate de date", + "Clear watch history": "Ștergeți istoricul videoclipurilor vizionate", + "Import/export data": "Importați/exportați datele", + "Change password": "Schimbați parola", + "Manage subscriptions": "Gestionați abonamentele", + "Manage tokens": "Gestionați tokenele", + "Watch history": "Istoricul videoclipurilor vizionate", + "Delete account": "Ștergeți contul", + "Administrator preferences": "Preferințele Administratorului", + "Default homepage: ": "Pagina principală implicită: ", + "Feed menu: ": "Preferințe legate de pagina de abonamente: ", + "Top enabled: ": "Top activat: ", + "CAPTCHA enabled: ": "CAPTCHA activat : ", + "Login enabled: ": "Autentificare activată : ", + "Registration enabled: ": "Înregistrate activată: ", + "Report statistics: ": "Raportarea statisticilor: ", + "Save preferences": "Salvați preferințele", + "Subscription manager": "Gestionați abonamentele", + "Token manager": "Manager de Tokene", + "Token": "Token", + "`x` subscriptions": "`x` abonamente", + "`x` tokens": "`x` tokens", + "Import/export": "Importați/Exportați", + "unsubscribe": "dezabonați-vă", + "revoke": "revocați", + "Subscriptions": "Abonamente", + "`x` unseen notifications": "`x` notificări nevăzute", + "search": "căutați", + "Log out": "Deconectați-vă", + "Released under the AGPLv3 by Omar Roth.": "Publicat sub licența AGPLv3 de Omar Roth.", + "Source available here.": "Codul sursă este disponibil aici.", + "View JavaScript license information.": "Informații legate de licența JavaScript.", + "View privacy policy.": "Politica de confidențialitate.", + "Trending": "Tendințe", + "Public": "Public", + "Unlisted": "Necatalogat", + "Private": "Privat", + "View all playlists": "Afișați toate listele de redare", + "Updated `x` ago": "Actualizat acum `x`", + "Delete playlist `x`?": "Sigur doriți să ștergeți lista de redare?", + "Delete playlist": "Ștergeți lista de redare", + "Create playlist": "Creați o listă de redare", + "Title": "Titlu", + "Playlist privacy": "Parametrii de confidențialitate ai listei de redare", + "Editing playlist `x`": "Modificați lista de redare `x`", + "Watch on YouTube": "Urmăriți videoclipul pe YouTube", + "Hide annotations": "Ascundeți adnotările", + "Show annotations": "Afișați adnotările", + "Genre: ": "Categorie: ", + "License: ": "Licență: ", + "Family friendly? ": "Adecvat pentru întreaga familie? ", + "Wilson score: ": "Scor Wilson: ", + "Engagement: ": "Procentul celor care au apăsat pe \"Îmi place\" sau \"Nu îmi place\" : ", + "Whitelisted regions: ": "Regiunile de pe lista albă: ", + "Blacklisted regions: ": "Regiunile de pe lista neagră: ", + "Shared `x`": "Publicat pe `x`", + "`x` views": "`x` vizionări", + "Premieres in `x`": "Premiera în `x`", + "Premieres `x`": "Premiera pe `x`", + "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Se pare că ați dezactivat JavaScript. Apăsați aici pentru a vizualiza comentariile. Țineți minte faptul că încărcarea lor ar putea să dureze puțin mai mult.", + "View YouTube comments": "Vedeți comentariile de pe YouTube", + "View more comments on Reddit": "Vedeți mai multe comentarii pe Reddit", + "View `x` comments": "Afișați `x` comentarii", + "View Reddit comments": "Afișați comentariile de pe Reddit", + "Hide replies": "Ascundeți replicile", + "Show replies": "Afișați replicile", + "Incorrect password": "Parolă incorectă", + "Quota exceeded, try again in a few hours": "Numărul de tentative de conectare a fost depășit. Va rugăm să încercați din nou în câteva ore.", + "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Conectare eșuată. Dacă nu reușiți să vă conectați, verificați dacă ați activat autentificarea cu doi factori (Autentificator sau SMS).", + "Invalid TFA code": "Codul de autentificare cu doi factori este invalid", + "Login failed. This may be because two-factor authentication is not turned on for your account.": "Conectare eșuată. Acest lucru ar putea fi cauzat de faptul că nu ați activat autentificarea cu doi factori.", + "Wrong answer": "Răspuns invalid", + "Erroneous CAPTCHA": "CAPTCHA invalid", + "CAPTCHA is a required field": "Câmpul CAPTCHA este obligatoriu", + "User ID is a required field": "Câmpul ID Utilizator este obligatoriu", + "Password is a required field": "Câmpul Parolă este obligatoriu", + "Wrong username or password": "Nume de utilizator sau parolă invalidă", + "Please sign in using 'Log in with Google'": "Vă rog conectați-vă folosind \"Conectați-vă cu Google\"", + "Password cannot be empty": "Parola nu poate fi goală", + "Password cannot be longer than 55 characters": "Parola nu poate să conțină mai mult de 55 de caractere", + "Please log in": "Vă rog conectați-vă", + "Invidious Private Feed for `x`": "Feed RSS privat pentru `x`", + "channel:`x`": "canal:`x`", + "Deleted or invalid channel": "Canal șters sau invalid", + "This channel does not exist.": "Acest canal nu există.", + "Could not get channel info.": "Nu am putut primi informații despre acest canal.", + "Could not fetch comments": "Încărcarea comentariilor a eșuat.", + "View `x` replies": "Afișați `x` replici", + "`x` ago": "acum `x`", + "Load more": "Vedeți mai mult", + "`x` points": "`x` puncte", + "Could not create mix.": "Nu am putut crea această listă de redare.", + "Empty playlist": "Lista de redare este goală", + "Not a playlist.": "Lista de redare este invalidă.", + "Playlist does not exist.": "Această listă de redare nu există.", + "Could not pull trending pages.": "Încărcarea paginilor de tendințe a eșuat.", + "Hidden field \"challenge\" is a required field": "Câmpul ascuns \"challenge\" este un câmp obligatoriu", + "Hidden field \"token\" is a required field": "Câmpul ascuns \"token\" este un câmp obligatoriu", + "Erroneous challenge": "Challenge invalid", + "Erroneous token": "Token invalid", + "No such user": "Acest utilizator nu există", + "Token is expired, please try again": "Token-ul este expirat, vă rugăm să reîncercați.", + "English": "Engleză", + "English (auto-generated)": "Engleză (generată automat)", + "Afrikaans": "Afrikaans", + "Albanian": "Albaneză", + "Amharic": "Amharică", + "Arabic": "Arabă", + "Armenian": "Arméniană", + "Azerbaijani": "Azeră", + "Bangla": "Bangla", + "Basque": "Basque", + "Belarusian": "Belarusă", + "Bosnian": "Bosniacă", + "Bulgarian": "Bulgară", + "Burmese": "Birmană", + "Catalan": "Catalană", + "Cebuano": "Cebuano", + "Chinese (Simplified)": "Chineză (Simplificată)", + "Chinese (Traditional)": "Chinois (Tradițională)", + "Corsican": "Corsicană", + "Croatian": "Croată", + "Czech": "Cehă", + "Danish": "Daneză", + "Dutch": "Olandeză", + "Esperanto": "Esperanto", + "Estonian": "Estoniană", + "Filipino": "Filipineză", + "Finnish": "Finlandeză", + "French": "Franceză", + "Galician": "Galiciană", + "Georgian": "Georgiană", + "German": "Germană", + "Greek": "Greacă", + "Gujarati": "Gujarati", + "Haitian Creole": "Creola Haitiană", + "Hausa": "Haousa", + "Hawaiian": "Hawaiană", + "Hebrew": "Ebraică", + "Hindi": "Hindi", + "Hmong": "Hmong", + "Hungarian": "Ungară", + "Icelandic": "Islandeză", + "Igbo": "Igbo", + "Indonesian": "Indoneziană", + "Irish": "Irlandeză", + "Italian": "Italiană", + "Japanese": "Japoneză", + "Javanese": "Javaneză", + "Kannada": "Kannada", + "Kazakh": "Kazakh", + "Khmer": "Khmer", + "Korean": "Coreană", + "Kurdish": "Kurdă", + "Kyrgyz": "Kirghize", + "Lao": "Lao", + "Latin": "Latină", + "Latvian": "Letonă", + "Lithuanian": "Lituaniană", + "Luxembourgish": "Luxemburgheză", + "Macedonian": "Macedoniană", + "Malagasy": "Malgașă", + "Malay": "Malaieză", + "Malayalam": "Malayalam", + "Maltese": "Malteză", + "Maori": "Maori", + "Marathi": "Marathi", + "Mongolian": "Mongoliană", + "Nepali": "Nepaleză", + "Norwegian Bokmål": "Norvegiană", + "Nyanja": "Nyanja", + "Pashto": "Pachtou", + "Persian": "Persană", + "Polish": "Poloneză", + "Portuguese": "Portugheză", + "Punjabi": "Punjabi", + "Romanian": "Română", + "Russian": "Rusă", + "Samoan": "Samoan", + "Scottish Gaelic": "Galic Scoțian", + "Serbian": "Sârbă", + "Shona": "Shona", + "Sindhi": "Sindhi", + "Sinhala": "Sinhala", + "Slovak": "Slovacă", + "Slovenian": "Slovenă", + "Somali": "Somaleză", + "Southern Sotho": "Sotho de Sud", + "Spanish": "Spaniolă", + "Spanish (Latin America)": "Spaniolă (America Latină)", + "Sundanese": "Sundaneză", + "Swahili": "Swahili", + "Swedish": "Suedeză", + "Tajik": "Tajik", + "Tamil": "Tamil", + "Telugu": "Telugu", + "Thai": "Tailandeză", + "Turkish": "Turcă", + "Ukrainian": "Ucrainiană", + "Urdu": "Urdu", + "Uzbek": "Uzbek", + "Vietnamese": "Vietnameză", + "Welsh": "Galeză", + "Western Frisian": "Frisiană de Vest", + "Xhosa": "Xhosa", + "Yiddish": "Yiddish", + "Yoruba": "Yoruba", + "Zulu": "Zoulou", + "`x` years": "`x` ani", + "`x` months": "`x` luni", + "`x` weeks": "`x` săptămâni", + "`x` days": "`x` zile", + "`x` hours": "`x` ore", + "`x` minutes": "`x` minute", + "`x` seconds": "`x` secunde", + "Fallback comments: ": "Comentarii alternative: ", + "Popular": "Popular", + "Top": "Top", + "About": "Despre", + "Rating: ": "Evaluare: ", + "Language: ": "Limbă: ", + "View as playlist": "Vizualizați ca listă de redare", + "Default": "Implicit", + "Music": "Muzică", + "Gaming": "Jocuri Video", + "News": "Noutăți", + "Movies": "Filme", + "Download": "Descărcați", + "Download as: ": "Descărcați ca: ", + "%A %B %-d, %Y": "%A %-d %B %Y", + "(edited)": "(editat)", + "YouTube comment permalink": "Permalink pentru comentariul de pe YouTube", + "permalink": "permalink", + "`x` marked it with a ❤": "`x` l-a marcat cu o ❤", + "Audio mode": "Mod audio", + "Video mode": "Mod video", + "Videos": "Videoclipuri", + "Playlists": "Liste de redare", + "Community": "Comunitate", + "Current version: ": "Versiunea actuală: " +} From 062867a38dc111fe981e2c65239b557191daa7fa Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Sun, 1 Dec 2019 17:52:39 -0500 Subject: [PATCH 0011/2930] Strip domain from caption URLs --- src/invidious.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious.cr b/src/invidious.cr index 6499d569..df6ea3e8 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -3852,7 +3852,7 @@ get "/api/v1/captions/:id" do |env| caption = caption[0] end - url = "#{caption.baseUrl}&tlang=#{tlang}" + url = URI.parse("#{caption.baseUrl}&tlang=#{tlang}").full_path # Auto-generated captions often have cues that aren't aligned properly with the video, # as well as some other markup that makes it cumbersome, so we try to fix that here From 823603650f7f45e7cdc27f0beb7d6e346efb6ec5 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Tue, 3 Dec 2019 19:14:11 -0500 Subject: [PATCH 0012/2930] Add support for /sorry/index CAPTCHA --- src/invidious/helpers/jobs.cr | 54 +++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/invidious/helpers/jobs.cr b/src/invidious/helpers/jobs.cr index 5838b5b3..f368d6df 100644 --- a/src/invidious/helpers/jobs.cr +++ b/src/invidious/helpers/jobs.cr @@ -290,6 +290,60 @@ def bypass_captcha(captcha_key, logger) response = YT_POOL.client &.post("/das_captcha", headers, form: inputs) yield response.cookies.select { |cookie| cookie.name != "PREF" } + elsif response.headers["Location"]?.try &.includes?("/sorry/index") + location = response.headers["Location"].try { |u| URI.parse(u) } + client = QUIC::Client.new(location.host.not_nil!) + response = client.get(location.full_path) + + html = XML.parse_html(response.body) + form = html.xpath_node(%(//form[@action="index"])).not_nil! + site_key = form.xpath_node(%(.//div[@class="g-recaptcha"])).try &.["data-sitekey"] + + inputs = {} of String => String + form.xpath_nodes(%(.//input[@name])).map do |node| + inputs[node["name"]] = node["value"] + end + + response = JSON.parse(HTTP::Client.post("https://api.anti-captcha.com/createTask", body: { + "clientKey" => CONFIG.captcha_key, + "task" => { + "type" => "NoCaptchaTaskProxyless", + "websiteURL" => location.to_s, + "websiteKey" => site_key, + }, + }.to_json).body) + + if response["error"]? + raise response["error"].as_s + end + + task_id = response["taskId"].as_i + + loop do + sleep 10.seconds + + response = JSON.parse(HTTP::Client.post("https://api.anti-captcha.com/getTaskResult", body: { + "clientKey" => CONFIG.captcha_key, + "taskId" => task_id, + }.to_json).body) + + if response["status"]?.try &.== "ready" + break + elsif response["errorId"]?.try &.as_i != 0 + raise response["errorDescription"].as_s + end + end + + inputs["g-recaptcha-response"] = response["solution"]["gRecaptchaResponse"].as_s + client.close + client = QUIC::Client.new("www.google.com") + response = client.post(location.full_path, form: inputs) + headers = HTTP::Headers{ + "Cookie" => URI.parse(response.headers["location"]).query_params["google_abuse"].split(";")[0], + } + cookies = HTTP::Cookies.from_headers(headers) + + yield cookies end rescue ex logger.puts("Exception: #{ex.message}") From 1499ce43bf1cd6a47e8c1b0df4c915feb7ca8fb4 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Tue, 3 Dec 2019 19:41:58 -0500 Subject: [PATCH 0013/2930] Add support for Romanian locale --- locales/ro.json | 5 ++--- src/invidious.cr | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/locales/ro.json b/locales/ro.json index 57c76a83..75496a01 100644 --- a/locales/ro.json +++ b/locales/ro.json @@ -1,5 +1,4 @@ { - "`x` subscribers": "`x` abonați", "`x` videos": "`x` videoclipuri", "`x` playlists": "`x` liste de redare", @@ -58,7 +57,7 @@ "Play next by default: ": "Vizionați următoarele videoclipuri în mod implicit: ", "Autoplay next video: ": "Porniți următorul videoclip automat: ", "Listen by default: ": "Numai audio: ", - "Proxy videos: " : "Redați videoclipurile printr-un proxy: ", + "Proxy videos: ": "Redați videoclipurile printr-un proxy: ", "Default speed: ": "Viteza de redare implicită: ", "Preferred video quality: ": "Calitatea videoclipurilor: ", "Player volume: ": "Volumul videoclipurilor: ", @@ -334,4 +333,4 @@ "Playlists": "Liste de redare", "Community": "Comunitate", "Current version: ": "Versiunea actuală: " -} +} \ No newline at end of file diff --git a/src/invidious.cr b/src/invidious.cr index df6ea3e8..a4f20bc4 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -87,6 +87,7 @@ LOCALES = { "nb_NO" => load_locale("nb_NO"), "nl" => load_locale("nl"), "pl" => load_locale("pl"), + "ro" => load_locale("ro"), "ru" => load_locale("ru"), "tr" => load_locale("tr"), "uk" => load_locale("uk"), From 28669d940a363bfb504a930184f9da27d08daa30 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Thu, 5 Dec 2019 14:49:44 -0500 Subject: [PATCH 0014/2930] Remove --release from dockerfile --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 1bb4d11f..11ab6ed2 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -19,7 +19,7 @@ COPY ./src/ ./src/ # See definition of CURRENT_BRANCH, CURRENT_COMMIT and CURRENT_VERSION. COPY ./.git/ ./.git/ RUN crystal build ./src/invidious.cr \ - --release --static --warnings all --error-on-warnings \ + --static --warnings all --error-on-warnings \ # TODO: Remove next line, see https://github.com/crystal-lang/crystal/issues/7946 -Dmusl \ --link-flags "-lxml2 -llzma" From 5751bb2481baad2965195e53fec6779ce6699fb1 Mon Sep 17 00:00:00 2001 From: Everton Date: Thu, 5 Dec 2019 17:24:53 -0300 Subject: [PATCH 0015/2930] Add Brazilian Portuguese locale (#915) * adding Brazilian Portuguese locale --- locales/pt-BR.json | 337 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 337 insertions(+) create mode 100644 locales/pt-BR.json diff --git a/locales/pt-BR.json b/locales/pt-BR.json new file mode 100644 index 00000000..d13a69ac --- /dev/null +++ b/locales/pt-BR.json @@ -0,0 +1,337 @@ +{ + "`x` subscribers": "`x` inscritos", + "`x` videos": "`x` videos", + "`x` playlists": "", + "LIVE": "AO VIVO", + "Shared `x` ago": "Compartilhado `x` atrás", + "Unsubscribe": "Desinscrever-se", + "Subscribe": "Inscrever-se", + "View channel on YouTube": "Ver canal no YouTube", + "View playlist on YouTube": "Ver lista de reprodução no YouTube", + "newest": "mais recentes", + "oldest": "mais antigos", + "popular": "populares", + "last": "último", + "Next page": "Próxima página", + "Previous page": "Página anterior", + "Clear watch history?": "Limpar histórico de reprodução?", + "New password": "Nova senha", + "New passwords must match": "Nova senha deve ser igual", + "Cannot change password for Google accounts": "Não é possível alterar sua senha da conta Google", + "Authorize token?": "Autorizar o token?", + "Authorize token for `x`?": "Autorizar o token para `x`?", + "Yes": "Sim", + "No": "Não", + "Import and Export Data": "Importar e Exportar Dados", + "Import": "Importar", + "Import Invidious data": "Importar datos do Invidious", + "Import YouTube subscriptions": "Importar inscrições do YouTube", + "Import FreeTube subscriptions (.db)": "Importar inscrições do FreeTube (.db)", + "Import NewPipe subscriptions (.json)": "Importar inscrições do NewPipe (.json)", + "Import NewPipe data (.zip)": "Importar dados do NewPipe (.zip)", + "Export": "Exportar", + "Export subscriptions as OPML": "Exportar inscrições como OPML", + "Export subscriptions as OPML (for NewPipe & FreeTube)": "Exportar inscrições como OPML (para NewPipe e FreeTube)", + "Export data as JSON": "Exportar dados como JSON", + "Delete account?": "Deletar conta?", + "History": "Histórico", + "An alternative front-end to YouTube": "Uma interface alternativa para o YouTube", + "JavaScript license information": "Informação de licença do JavaScript", + "source": "código fonte", + "Log in": "Entrar", + "Log in/register": "Entrar/Registrar", + "Log in with Google": "Entrar com conta Google", + "User ID": "Usuário", + "Password": "Senha", + "Time (h:mm:ss):": "Hora (h:mm:ss):", + "Text CAPTCHA": "CAPTCHA em texto", + "Image CAPTCHA": "CAPTCHA em imagen", + "Sign In": "Entrar", + "Register": "Registrar", + "E-mail": "E-mail", + "Google verification code": "Código de verificação do Google", + "Preferences": "Preferências", + "Player preferences": "Preferências do reprodutor", + "Always loop: ": "Repetir sempre: ", + "Autoplay: ": "Reprodução automática: ", + "Play next by default: ": "Sempre reproduzir próximo: ", + "Autoplay next video: ": "Reproduzir próximo video automaticamente: ", + "Listen by default: ": "Sempre ativar som: ", + "Proxy videos: ": "Usar proxy nos videos: ", + "Default speed: ": "Velocidade preferida: ", + "Preferred video quality: ": "Qualidade de video preferida: ", + "Player volume: ": "Volume de reprodução: ", + "Default comments: ": "Preferência de comentários: ", + "youtube": "youtube", + "reddit": "reddit", + "Default captions: ": "Preferência de legendas: ", + "Fallback captions: ": "Legendas alternativas: ", + "Show related videos: ": "Ver videos relacionados: ", + "Show annotations by default: ": "Sempre mostrar anotações: ", + "Visual preferences": "Preferências visuais", + "Player style: ": "Estilo do reprodutor", + "Dark mode: ": "Modo escuro: ", + "Theme: ": "Tema", + "dark": "escuro", + "light": "claro", + "Thin mode: ": "Modo compacto: ", + "Subscription preferences": "Preferências de inscrições", + "Show annotations by default for subscribed channels: ": "Sempre mostrar anotações nos videos de canais inscritos ", + "Redirect homepage to feed: ": "Redirecionar página inicial para o feed: ", + "Number of videos shown in feed: ": "Número de videos no feed: ", + "Sort videos by: ": "Ordenar videos por: ", + "published": "publicado", + "published - reverse": "publicado - ordem inversa", + "alphabetically": "alfabética", + "alphabetically - reverse": "alfabética - ordem inversa", + "channel name": "nome do canal", + "channel name - reverse": "nome do canal - ordem inversa", + "Only show latest video from channel: ": "Mostrar apenas o video mais recente do canal: ", + "Only show latest unwatched video from channel: ": "Mostrar apenas o video mais recente não visualizados do canal: ", + "Only show unwatched: ": "Mostrar apenas videos não visualizados: ", + "Only show notifications (if there are any): ": "Mostrar apenas notificações (se existentes): ", + "Enable web notifications": "Ativar notificações pela web", + "`x` uploaded a video": "`x` publicou um novo video", + "`x` is live": "`x` está ao vivo", + "Data preferences": "Preferências de dados", + "Clear watch history": "Limpar histórico de reprodução", + "Import/export data": "Importar/Exportar dados", + "Change password": "Alterar senha", + "Manage subscriptions": "Gerenciar inscrições", + "Manage tokens": "Gerenciar tokens", + "Watch history": "Histórico de reprodução", + "Delete account": "Apagar sua conta", + "Administrator preferences": "Preferências de administrador", + "Default homepage: ": "Página de inicio padrão: ", + "Feed menu: ": "Menú do feed: ", + "Top enabled: ": "Habilitar destaques: ", + "CAPTCHA enabled: ": "Habilitar CAPTCHA: ", + "Login enabled: ": "Habilitar login: ", + "Registration enabled: ": "Habilitar registro: ", + "Report statistics: ": "Habilitar estatísticas: ", + "Save preferences": "Salvar preferências", + "Subscription manager": "Gerenciador de inscrições", + "Token manager": "Gerenciador de tokens", + "Token": "Token", + "`x` subscriptions": "`x` inscrições", + "`x` tokens": "`x` tokens", + "Import/export": "Importar/Exportar", + "unsubscribe": "desinscrever-se", + "revoke": "revogar", + "Subscriptions": "Inscrições", + "`x` unseen notifications": "`x` notificações não visualizadas", + "search": "procurar", + "Log out": "Sair", + "Released under the AGPLv3 by Omar Roth.": "Publicado sob a licença AGPLv3, por Omar Roth.", + "Source available here.": "Código fonte disponível aqui.", + "View JavaScript license information.": "Ver informações da licença do JavaScript.", + "View privacy policy.": "Ver a política de privacidade", + "Trending": "Trending", + "Public": "Público", + "Unlisted": "No listado", + "Private": "Privado", + "View all playlists": "Mostrar todas listas de reprodução", + "Updated `x` ago": "Enviado `x` atrás", + "Delete playlist `x`?": "Apagar a playlist `x`?", + "Delete playlist": "Apagar playlist", + "Create playlist": "Criar playlist", + "Title": "Título", + "Playlist privacy": "Privacidade da playlist", + "Editing playlist `x`": "Editando playlist", + "Watch on YouTube": "Assistir vídeo no YouTube", + "Hide annotations": "Ocultar anotações", + "Show annotations": "Mostrar anotações", + "Genre: ": "Gênero: ", + "License: ": "Licença: ", + "Family friendly? ": "Fistrar conteúdo impróprio: ", + "Wilson score: ": "Pontuação de Wilson: ", + "Engagement: ": "Engagement: ", + "Whitelisted regions: ": "Regiões permitidas: ", + "Blacklisted regions: ": "Regiões bloqueadas: ", + "Shared `x`": "Compartilhado `x`", + "`x` views": "`x` visualizações", + "Premieres in `x`": "Estreias em `x`", + "Premieres `x`": "Estreia `x`", + "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Oi! Parece que seu JavaScript está desativado. Clique aqui para ver os comentários, entretanto eles podem levar um pouco mais de tempo para carregar.", + "View YouTube comments": "Ver comentários do YouTube", + "View more comments on Reddit": "Ver mais comentários do Reddit", + "View `x` comments": "Ver `x` comentários", + "View Reddit comments": "Ver comentários do Reddit", + "Hide replies": "Ocultar respostas", + "Show replies": "Mostrar respostas", + "Incorrect password": "Senha incorreta", + "Quota exceeded, try again in a few hours": "Cota excedida, tente novamente em algumas horas", + + "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Não foi possível fazer login, sua autenticação por dois passos (app autenticador ou sms) deve estar ativada.", + "Invalid TFA code": "Código TFA inválido", + "Login failed. This may be because two-factor authentication is not turned on for your account.": "Falha no login. Isso pode acontecer pois a autenticação por dois passos está desativada para sua conta.", + "Wrong answer": "Respuesta inválida", + "Erroneous CAPTCHA": "CAPTCHA inválido", + "CAPTCHA is a required field": "O CAPTCHA é um campo obrigatório", + "User ID is a required field": "O nome de usuário é um campo obrigatório", + "Password is a required field": "A senha é um campo obrigatório", + "Wrong username or password": "Nome de usuário ou senha inválidos", + "Please sign in using 'Log in with Google'": "Por favor, entre usando 'Entrar com conta Google'", + "Password cannot be empty": "A senha não pode estar vazia", + "Password cannot be longer than 55 characters": "A senha não pode ter mais que 55 caracteres", + "Please log in": "Por favor, inicie sua seção", + "Invidious Private Feed for `x`": "Feed Privado do Invidious para `x`", + "channel:`x`": "canal: `x`", + "Deleted or invalid channel": "Este canal foi apagado ou é inválido", + "This channel does not exist.": "Este canal não existe.", + "Could not get channel info.": "Não foi possível obter as informações do canal.", + "Could not fetch comments": "Não foi possível obter os comentários", + "View `x` replies": "Ver `x` respostas", + "`x` ago": "`x` atrás", + "Load more": "Carregar mais", + "`x` points": "`x` pontos", + "Could not create mix.": "Não foi possível criar o mix.", + "Empty playlist": "A lista de reprodução está vazia", + "Not a playlist.": "Lista de reprodução inválida.", + "Playlist does not exist.": "A lista de reprodução não existe.", + "Could not pull trending pages.": "Não foi possível oberter as páginas dos videos em alta.", + "Hidden field \"challenge\" is a required field": "O campo oculto \"desafio\" é obrigatório", + "Hidden field \"token\" is a required field": "O campo oculto \"token\" é obrigatório", + "Erroneous challenge": "Desafío inválido", + "Erroneous token": "Símbolo inválido", + "No such user": "Usuario inválido", + "Token is expired, please try again": "Token expirou, tente novamente", + "English": "Inglês", + "English (auto-generated)": "Inglês (gerado automaticamente)", + "Afrikaans": "Africâner", + "Albanian": "Albanês", + "Amharic": "Amárico", + "Arabic": "Árabe", + "Armenian": "Armênio", + "Azerbaijani": "Azeri", + "Bangla": "Bengalês", + "Basque": "Basco", + "Belarusian": "Bielorrusso", + "Bosnian": "Língua Bósnia", + "Bulgarian": "Búlgaro", + "Burmese": "Birmanês", + "Catalan": "Catalão", + "Cebuano": "Cebuano", + "Chinese (Simplified)": "Chinês Simplificado", + "Chinese (Traditional)": "Chinês Tradicional", + "Corsican": "Corso", + "Croatian": "Croata", + "Czech": "Checo", + "Danish": "Dinamarquês", + "Dutch": "Holandês", + "Esperanto": "Esperanto", + "Estonian": "Estoniano", + "Filipino": "Filipino", + "Finnish": "Finlandês", + "French": "Francês", + "Galician": "Galego", + "Georgian": "Georgiano", + "German": "Alemão", + "Greek": "Grego", + "Gujarati": "Guzerate", + "Haitian Creole": "Crioulo Haitiano", + "Hausa": "Hauçá", + "Hawaiian": "Havaiano", + "Hebrew": "Hebraico", + "Hindi": "Hindi", + "Hmong": "Hmong", + "Hungarian": "Húngaro", + "Icelandic": "Islandês", + "Igbo": "Igbo", + "Indonesian": "Indonésio", + "Irish": "Irlandês", + "Italian": "Italiano", + "Japanese": "Japonês", + "Javanese": "Javanês", + "Kannada": "Canarẽs", + "Kazakh": "Cazaque", + "Khmer": "Khmer", + "Korean": "Coreano", + "Kurdish": "Curdo", + "Kyrgyz": "Quirguiz", + "Lao": "Laosiano", + "Latin": "Latim", + "Latvian": "Letão", + "Lithuanian": "Lituano", + "Luxembourgish": "Luxemburguês", + "Macedonian": "Macedônio", + "Malagasy": "Malgaxe", + "Malay": "Malaia", + "Malayalam": "Malaiala", + "Maltese": "Maltês", + "Maori": "Maori", + "Marathi": "Marathi", + "Mongolian": "Mongol", + "Nepali": "Nepalês", + "Norwegian Bokmål": "Bokmål Norueguês", + "Nyanja": "Nianja", + "Pashto": "Pachto", + "Persian": "Persa", + "Polish": "Polaco", + "Portuguese": "Português", + "Punjabi": "Panjábi", + "Romanian": "Língua Romena", + "Russian": "Russo", + "Samoan": "Samoano", + "Scottish Gaelic": "Ânglico Escocês", + "Serbian": "Língua Sérvia", + "Shona": "Xona", + "Sindhi": "Sindi", + "Sinhala": "Cingalês", + "Slovak": "Eslovaco", + "Slovenian": "Esloveno", + "Somali": "Língua Somalí", + "Southern Sotho": "Sesoto", + "Spanish": "Espanhol", + "Spanish (Latin America)": "Espanhol (América)", + "Sundanese": "Sondanese", + "Swahili": "Suaíli", + "Swedish": "Suéco", + "Tajik": "Tajiques", + "Tamil": "Tâmil", + "Telugu": "Telugo", + "Thai": "Tailandês", + "Turkish": "Turco", + "Ukrainian": "Ucraniano", + "Urdu": "Urdu", + "Uzbek": "Uzbeque", + "Vietnamese": "Vietnamita", + "Welsh": "Galês", + "Western Frisian": "Língua Frísia", + "Xhosa": "Xhosa", + "Yiddish": "Iídiche", + "Yoruba": "Iorubá", + "Zulu": "Língua Zulú", + "`x` years": "`x` anos", + "`x` months": "`x` meses", + "`x` weeks": "`x` semanas", + "`x` days": "`x` dias", + "`x` hours": "`x` horas", + "`x` minutes": "`x` minutos", + "`x` seconds": "`x` segundos", + "Fallback comments: ": "Comentários alternativos: ", + "Popular": "Populares", + "Top": "No topo", + "About": "Sobre", + "Rating: ": "Avaliação: ", + "Language: ": "Idioma: ", + "View as playlist": "Ver como lista de reprodução", + "Default": "Configuração padrão", + "Music": "Música", + "Gaming": "Video Games", + "News": "Notícias", + "Movies": "Filmes", + "Download": "Baixar", + "Download as: ": "Baixar como: ", + "%A %B %-d, %Y": "%A %-d %B %Y", + "(edited)": "(editado)", + "YouTube comment permalink": "Link permanente do comentário do YouTube", + "permalink": "", + "`x` marked it with a ❤": "`x` foi marcado como ❤", + "Audio mode": "Modo de audio", + "Video mode": "Modo de video", + "Videos": "Videos", + "Playlists": "Listas de reprodução", + "Community": "Comunidade", + "Current version: ": "Versão atual: " +} \ No newline at end of file From 0e844edacb45c803d5142bf63d35000fbbefce8d Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Thu, 5 Dec 2019 15:26:35 -0500 Subject: [PATCH 0016/2930] Add support for pt-BR --- locales/{nb_NO.json => nb-NO.json} | 0 locales/pt-BR.json | 1 - src/invidious.cr | 3 ++- 3 files changed, 2 insertions(+), 2 deletions(-) rename locales/{nb_NO.json => nb-NO.json} (100%) diff --git a/locales/nb_NO.json b/locales/nb-NO.json similarity index 100% rename from locales/nb_NO.json rename to locales/nb-NO.json diff --git a/locales/pt-BR.json b/locales/pt-BR.json index d13a69ac..5a4bcfc8 100644 --- a/locales/pt-BR.json +++ b/locales/pt-BR.json @@ -161,7 +161,6 @@ "Show replies": "Mostrar respostas", "Incorrect password": "Senha incorreta", "Quota exceeded, try again in a few hours": "Cota excedida, tente novamente em algumas horas", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Não foi possível fazer login, sua autenticação por dois passos (app autenticador ou sms) deve estar ativada.", "Invalid TFA code": "Código TFA inválido", "Login failed. This may be because two-factor authentication is not turned on for your account.": "Falha no login. Isso pode acontecer pois a autenticação por dois passos está desativada para sua conta.", diff --git a/src/invidious.cr b/src/invidious.cr index a4f20bc4..5d011a5c 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -84,8 +84,9 @@ LOCALES = { "is" => load_locale("is"), "it" => load_locale("it"), "ja" => load_locale("ja"), - "nb_NO" => load_locale("nb_NO"), + "nb-NO" => load_locale("nb-NO"), "nl" => load_locale("nl"), + "pt-BR" => load_locale("pt-BR"), "pl" => load_locale("pl"), "ro" => load_locale("ro"), "ru" => load_locale("ru"), From 513363504f28d32c9bfb0d325296701f660b766a Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Thu, 5 Dec 2019 15:46:21 -0500 Subject: [PATCH 0017/2930] Add better error message for fetch_channel --- src/invidious/channels.cr | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/invidious/channels.cr b/src/invidious/channels.cr index 10e826e5..ccb87d6f 100644 --- a/src/invidious/channels.cr +++ b/src/invidious/channels.cr @@ -215,7 +215,17 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil) url = produce_channel_videos_url(ucid, page, auto_generated: auto_generated) response = YT_POOL.client &.get(url) - json = JSON.parse(response.body) + + begin + json = JSON.parse(response.body) + rescue ex + if response.body.includes?("To continue with your YouTube experience, please fill out the form below.") || + response.body.includes?("https://www.google.com/sorry/index") + raise "Could not extract channel info. Instance is likely blocked." + end + + raise "Could not extract JSON" + end if json["content_html"]? && !json["content_html"].as_s.empty? document = XML.parse_html(json["content_html"].as_s) From 88a538e71bd9810a8b47524b2ddc865ec6326285 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Thu, 5 Dec 2019 15:47:35 -0500 Subject: [PATCH 0018/2930] Minor refactor for channel playlists --- src/invidious/channels.cr | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/invidious/channels.cr b/src/invidious/channels.cr index ccb87d6f..433fe074 100644 --- a/src/invidious/channels.cr +++ b/src/invidious/channels.cr @@ -383,7 +383,7 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil) end def fetch_channel_playlists(ucid, author, auto_generated, continuation, sort_by) - if continuation + if continuation || auto_generated url = produce_channel_playlists_url(ucid, continuation, sort_by, auto_generated) response = YT_POOL.client &.get(url) @@ -402,13 +402,6 @@ def fetch_channel_playlists(ucid, author, auto_generated, continuation, sort_by) html = XML.parse_html(json["content_html"].as_s) nodeset = html.xpath_nodes(%q(//li[contains(@class, "feed-item-container")])) - elsif auto_generated - url = "/channel/#{ucid}" - - response = YT_POOL.client &.get(url) - html = XML.parse_html(response.body) - - nodeset = html.xpath_nodes(%q(//ul[@id="browse-items-primary"]/li[contains(@class, "feed-item-container")])) else url = "/channel/#{ucid}/playlists?disable_polymer=1&flow=list&view=1" @@ -504,10 +497,10 @@ def produce_channel_playlists_url(ucid, cursor, sort = "newest", auto_generated }, } - if !auto_generated - cursor = Base64.urlsafe_encode(cursor, false) + if cursor + cursor = Base64.urlsafe_encode(cursor, false) if !auto_generated + object["80226972:embedded"]["3:base64"].as(Hash)["15:string"] = cursor end - object["80226972:embedded"]["3:base64"].as(Hash)["15:string"] = cursor if auto_generated object["80226972:embedded"]["3:base64"].as(Hash)["4:varint"] = 0x32_i64 @@ -807,7 +800,7 @@ def produce_channel_community_continuation(ucid, cursor) object = { "80226972:embedded" => { "2:string" => ucid, - "3:string" => cursor, + "3:string" => cursor || "", }, } From 2c9e4ded40b3ada8066e9dc58d8de842aac21554 Mon Sep 17 00:00:00 2001 From: frajibe Date: Sat, 14 Dec 2019 18:20:26 +0100 Subject: [PATCH 0019/2930] Fix the french translation --- locales/fr.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index 928904c1..fa82c4c5 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -87,8 +87,8 @@ "channel name": "nom de la chaîne", "channel name - reverse": "nom de la chaîne - inversé", "Only show latest video from channel: ": "Afficher uniquement la dernière vidéo des chaînes auxquelles vous êtes abonnés : ", - "Only show latest unwatched video from channel: ": "Afficher uniquement la dernière vidéo des chaînes auxquelles vous êtes abonnés qui n'a pas était regardée : ", - "Only show unwatched: ": "Afficher uniquement les vidéos qui n'ont pas étaient regardées : ", + "Only show latest unwatched video from channel: ": "Afficher uniquement la dernière vidéo des chaînes auxquelles vous êtes abonnés qui n'a pas été regardée : ", + "Only show unwatched: ": "Afficher uniquement les vidéos qui n'ont pas été regardées : ", "Only show notifications (if there are any): ": "Afficher uniquement les notifications (s'il y en a) : ", "Enable web notifications": "Activer les notifications web", "`x` uploaded a video": "`x` a partagé(e) une vidéo", @@ -152,7 +152,7 @@ "`x` views": "`x` vues", "Premieres in `x`": "Première dans `x`", "Premieres `x`": "Première le `x`", - "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Il semblerait que JavaScript soit désactivé. Cliquez ici pour voir les commentaires sans. Gardez à l'esprit que le chargement peut prendre plus de temps.", + "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Il semblerait que JavaScript soit désactivé. Cliquez ici pour voir les commentaires, mais gardez à l'esprit que le chargement peut prendre plus de temps.", "View YouTube comments": "Voir les commentaires YouTube", "View more comments on Reddit": "Voir plus de commentaires sur Reddit", "View `x` comments": "Voir `x` commentaires", @@ -160,7 +160,7 @@ "Hide replies": "Masquer les réponses", "Show replies": "Afficher les réponses", "Incorrect password": "Mot de passe incorrect", - "Quota exceeded, try again in a few hours": "Nombre de tentative de connexion dépassée, réessayez dans quelques heures", + "Quota exceeded, try again in a few hours": "Nombre de tentatives de connexion dépassé, réessayez dans quelques heures", "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Impossible de se connecter, si après plusieurs tentative vous ne parvenez toujours pas à vous connecter, assurez-vous que l'authentification à deux facteurs (Authenticator ou SMS) est activée.", "Invalid TFA code": "Code d'authentification à deux facteurs invalide", "Login failed. This may be because two-factor authentication is not turned on for your account.": "La connexion a échoué. Cela peut être dû au fait que l'authentification à deux facteurs n'est pas activée sur votre compte.", From 588fc6df85e4bdcd1caff3c3d544a55a234362ee Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Sat, 14 Dec 2019 16:10:46 -0500 Subject: [PATCH 0020/2930] Bump dependencies --- shard.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/shard.yml b/shard.yml index d5901a78..e8b8d189 100644 --- a/shard.yml +++ b/shard.yml @@ -17,7 +17,7 @@ dependencies: version: ~> 0.14.0 kemal: github: kemalcr/kemal - version: ~> 0.26.0 + version: ~> 0.26.1 pool: github: ysbaddaden/pool version: ~> 0.2.3 @@ -26,8 +26,8 @@ dependencies: version: ~> 0.1.2 lsquic: github: omarroth/lsquic.cr - version: ~> 0.1.7 + version: ~> 0.1.8 -crystal: 0.31.1 +crystal: 0.32.0 license: AGPLv3 From 58c1a68ad9023581555b4d9c943dfd447b5c92bf Mon Sep 17 00:00:00 2001 From: Jorge Maldonado Ventura Date: Sat, 4 Jan 2020 15:27:45 +0100 Subject: [PATCH 0021/2930] Change embed code --- assets/js/player.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/player.js b/assets/js/player.js index e58af0cd..79788b1e 100644 --- a/assets/js/player.js +++ b/assets/js/player.js @@ -35,7 +35,7 @@ var shareOptions = { title: player_data.title, description: player_data.description, image: player_data.thumbnail, - embedCode: "" + embedCode: "" } var player = videojs('player', options); From 0560d2cfb7961b229bb0f91c869895e38f31e36d Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Wed, 8 Jan 2020 20:19:47 -0500 Subject: [PATCH 0022/2930] Bump video.js --- assets/css/video-js.min.css | 2 +- assets/js/video.min.js | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/assets/css/video-js.min.css b/assets/css/video-js.min.css index 68c82db0..b453b88c 100644 --- a/assets/css/video-js.min.css +++ b/assets/css/video-js.min.css @@ -1 +1 @@ -@charset "UTF-8";.video-js .vjs-big-play-button .vjs-icon-placeholder:before,.video-js .vjs-modal-dialog,.vjs-button>.vjs-icon-placeholder:before,.vjs-modal-dialog .vjs-modal-dialog-content{position:absolute;top:0;left:0;width:100%;height:100%}.video-js .vjs-big-play-button .vjs-icon-placeholder:before,.vjs-button>.vjs-icon-placeholder:before{text-align:center}@font-face{font-family:VideoJS;src:url(data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAABDkAAsAAAAAG6gAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABCAAAADsAAABUIIslek9TLzIAAAFEAAAAPgAAAFZRiV3hY21hcAAAAYQAAADaAAADPv749/pnbHlmAAACYAAAC3AAABHQZg6OcWhlYWQAAA3QAAAAKwAAADYZw251aGhlYQAADfwAAAAdAAAAJA+RCLFobXR4AAAOHAAAABMAAACM744AAGxvY2EAAA4wAAAASAAAAEhF6kqubWF4cAAADngAAAAfAAAAIAE0AIFuYW1lAAAOmAAAASUAAAIK1cf1oHBvc3QAAA/AAAABJAAAAdPExYuNeJxjYGRgYOBiMGCwY2BycfMJYeDLSSzJY5BiYGGAAJA8MpsxJzM9kYEDxgPKsYBpDiBmg4gCACY7BUgAeJxjYGS7wTiBgZWBgaWQ5RkDA8MvCM0cwxDOeI6BgYmBlZkBKwhIc01hcPjI+FGJHcRdyA4RZgQRADK3CxEAAHic7dFZbsMgAEXRS0ycyZnnOeG7y+qC8pU1dHusIOXxuoxaOlwZYWQB0Aea4quIEN4E9LzKbKjzDeM6H/mua6Lmc/p8yhg0lvdYx15ZG8uOLQOGjMp3EzqmzJizYMmKNRu27Nhz4MiJMxeu3Ljz4Ekqm7T8P52G8PP3lnTOVk++Z6iN6QZzNN1F7ptuN7eGOjDUoaGODHVsuvU8MdTO9Hd5aqgzQ50b6sJQl4a6MtS1oW4MdWuoO0PdG+rBUI+GejLUs6FeDPVqqDdDvRvqw1CfhpqM9At0iFLaAAB4nJ1YDXBTVRZ+5/22TUlJ8we0pHlJm7RJf5O8F2j6EymlSPkpxaL8U2xpa3DKj0CBhc2IW4eWKSokIoLsuMqssM64f+jA4HSdWXXXscBq67IOs3FXZ1ZYWVyRFdo899yXtIBQZ90k7717zz3v3HPPOfd854YCCj9cL9dL0RQFOqCbGJnrHb5EayiKIWN8iA/hWBblo6hUWm8TtCDwE80WMJus/irwyxOdxeB0MDb14VNJHnXYoLLSl6FfCUYO9nYPTA8Epg9090LprfbBbZ2hY0UlJUXHQp3/vtWkS6EBv8+rPMq5u9692f/dNxJNiqwC1xPE9TCUgCsSdQWgE3XQD25lkG4CN2xmTcOXWBOyser6RN6KnGbKSbmQ3+d0OI1m2W8QzLLkI2sykrWAgJJEtA8vGGW/2Q+CmT3n8zS9wZwu2DCvtuZKZN3xkrLh36yCZuUomQSqGpY8t/25VfHVhw8z4ebGBtfLb0ya9PCaDc+8dGTvk2dsh6z7WzvowlXKUSWo9MJ15a3KrEP2loOr2Ojhw6iW6hf2BDdEccQvZGpaAy7YovSwq8kr7HGllxpd71rkS6G0Sf11sl9OvMK1+jwPPODxjUwkOim9CU3ix1wNjXDfmJSEn618Bs6lpWwUpU+8PCqLMY650zjq8VhCIP17NEKTx3eaLL+s5Pi6yJWaWjTHLR1jYzPSV9VF/6Ojdb/1kO3Mk3uhHC0x6gc1BjlKQ+nQFxTYdaJkZ7ySVxLBbhR1dsboNXp1tCYKW2LRaEzpYcIx2BKNxaL0ZaUnSqfFoiNhHKR/GkX6PWUSAaJelQaqZL1EpoHNsajSEyPSoJ9IjhIxTdjHLmwZvhRDOiFTY/YeQnvrVZmiTQtGncECXtFTBZLOVwwMRgoXHAkXzMzPn1nAJJ8jYSbMDaqN2waGLzNhih/bZynUBMpIWSg7VYi7DRx2m8ALkIdRCJwI6ArJx2EI8kaDWeTQKeAFk9fjl/1AvwktjQ1P7NjyMGQyfd4vjipX6M/i52D7Cq80kqlcxEcGXRr/FEcgs0u5uGgB4VWuMFfpdn2Re6Hi3PqzmxWKsz6+ae2Pn9hXXw/fqM859UiGC0oKYYILJBqJrsn1Z1E5qOs9rQCiUQRREjm8yJcbHF5cUJufX1vAHlefw0XgUoboS3ETfQlTxBC4SOtuE8VPRJTBSCQSjZCpk7Gqzu+masaZ2y7Zjehho4F3g82BNDkAHpORG4+OCS+f6JTPmtRn/PH1kch6d04sp7AQb25aQ/pqUyXeQ8vrebG8OYQdXOQ+585u0sdW9rqalzRURiJ+9F4MweRFrKUjl1GUYhH1A27WOHw5cTFSFPMo9EeUIGnQTZHIaJ7AHLaOKsOODaNF9jkBjYG2QEsQ2xjMUAx2bBEbeTBWMHwskBjngq56S/yfgkBnWBa4K9sqKtq2t1UI8S9He5XuBRbawAdatrQEAi30Aks2+LM8WeCbalVZkWNylvJ+dqJnzVb+OHlSoKW8nPCP7Rd+CcZ2DdWAGqJ2CBFOphgywFFCFBNtfAbGtNPBCwxvygHeYMZMY9ZboBqwq/pVrsbgN5tkv152ODlbMfiqwGMBgxa4Exz3QhovRIUp6acqZmQzRq0ypDXS2TPLT02YIkQETnOE445oOGxOmXAqUJNNG7XgupMjPq2ua9asrj5yY/yuKteO1Kx0YNJTufrirLe1mZnat7OL6rnUdCWenpW6I8mAnbsY8KWs1PuSovCW9A/Z25PQ24a7cNOqgmTkLmBMgh4THgc4b9k2IVv1/g/F5nGljwPLfOgHAzJzh45V/4+WenTzmMtR5Z7us2Tys909UHqrPY7KbckoxRvRHhmVc3cJGE97uml0R1S0jdULVl7EvZtDFVBF35N9cEdjpgmAiOlFZ+Dtoh93+D3zzHr8RRNZQhnCNMNbcegOvpEwZoL+06cJQ07h+th3fZ/7PVbVC6ngTAV/KoLFuO6+2KFcU651gEb5ugPSIb1D+Xp8V4+k3sEIGnw5mYe4If4k1lFYr6SCzmM2EQ8iWtmwjnBI9kTwe1TlfAmXh7H02by9fW2gsjKwtv0aaURKil4OdV7rDL1MXIFNrhdxohcZXYTnq47WisrKitaObbf5+yvkLi5J6lCNZZ+B6GC38VNBZBDidSS/+mSvh6s+srgC8pyKMvDtt+de3c9fU76ZPfuM8ud4Kv0fyP/LqfepMT/3oZxSqpZaTa1DaQYLY8TFsHYbWYsPoRhRWfL5eSSQbhUGgGC3YLbVMk6PitTFNGpAsNrC6D1VNBKgBHMejaiuRWEWGgsSDBTJjqWIl8kJLlsaLJ2tXDr6xGfT85bM2Q06a46x2HTgvdnV8z5YDy/27J4zt6x2VtkzjoYpkq36kaBr4eQSg7tyiVweWubXZugtadl58ydapfbORfKsDTuZ0OBgx4cfdjCf5tbWNITnL120fdOi1RV1C3uKGzNdwYLcMvZ3BxoPyTOCD1XvXTp7U10gWCVmTV9b3r2z0SkGWovb2hp9I89O8a2smlyaO8muMU+dRmtzp60IzAoFpjLr1n388boLyf0dRvxhsHZ0qbWqDkwqvvpkj4l0fY6EIXRi5sQSrAvsVYwXRy4qJ2EVtD1AN7a0HWth9ymvL1xc3WTUKK/TAHA/bXDVtVWfOMfuGxGZv4Ln/jVr9jc3j1yMv0tndmyt9Vq88Y9gH1wtLX3KWjot5++jWHgAoZZkQ14wGQ20Fli71UmKJAy4xKMSTGbVdybW7FDDAut9XpD5AzWrYO7zQ8qffqF8+Ynd/clrHcdyxGy3a/3+mfNnzC/cBsveTjnTvXf1o6vzOlZw7WtqtdmPK/Errz/6NNtD72zmNOZfbmYdTGHfoofqI79Oc+R2n1lrnL6pOm0Up7kwxhTW12Amm7WYkXR2qYrF2AmgmbAsxZjwy1xpg/m1Je2vrp8v/nz2xpmlBg4E9hrMU341wVpTOh/OfmGvAnra8q6uctr60ZQHV3Q+WMQJykMj8ZsWn2QBOmmHMB+m5pDIpTFonYigiaKAhGEiAHF7EliVnQkjoLVIMPtJpBKHYd3A8GYH9jJzrWwmHx5Qjp7vDAX0suGRym1vtm/9W1/HyR8vczfMs6Sk8DSv855/5dlX9oQq52hT8syyp2rx5Id17IAyAM3wIjQPMOHzytEB64q6D5zT91yNbnx3V/nqnd017S9Y0605k3izoXLpsxde2n38yoOV9s1LcjwzNjbdX6asnBVaBj/6/DwKwPkpcqbDG7BnsXoSqWnUAmottYF6jMSdVyYZh3zVXCjwTiwwHH6sGuRiEHQGzuRX6whZkp123oy1BWE2mEfJ/tvIRtM4ZM5bDXiMsPMaAKOTyc5uL57rqyyc5y5JE5pm1i2S2iUX0CcaQ6lC6Zog7JqSqZmYlosl2K6pwNA84zRnQW6SaALYZQGW5lhCtU/W34N6o+bKfZ8cf3/Cl/+iTX3wBzpOY4mRkeNf3rptycGSshQWgGbYt5jFc2e0+DglIrwl6DVWQ7BuwaJ3Xk1J4VL5urnLl/Wf+gHU/hZoZdKNym6lG+I34FaNeZKcSpJIo2IeCVvpdsDGfKvzJnAwmeD37Ow65ZWwSowpgwX5T69s/rB55dP5BcpgDKFV8p7q2sn/1uc93bVzT/w6UrCqDTWvfCq/oCD/qZXNoUj8BL5Kp6GU017frfNXkAtiiyf/SOCEeLqnd8R/Ql9GlCRfctS6k5chvIBuQ1zCCjoCHL2DHNHIXxMJ3kQeO8lbsUXONeSfA5EjcG6/E+KdhN4bP04vBhdi883+BFBzQbxFbvZzQeY9LNBZc0FNfn5NwfDn6rCTnTw6R8o+gfpf5hCom33cRuiTlss3KHmZjD+BPN+5gXuA2ziS/Q73mLxUkpbKN/eqwz5uK0X9F3h2d1V4nGNgZGBgAOJd776+iue3+crAzc4AAje5Bfcg0xz9YHEOBiYQBQA8FQlFAHicY2BkYGBnAAGOPgaG//85+hkYGVCBMgBGGwNYAAAAeJxjYGBgYB8EmKOPgQEAQ04BfgAAAAAAAA4AaAB+AMwA4AECAUIBbAGYAcICGAJYArQC4AMwA7AD3gQwBJYE3AUkBWYFigYgBmYGtAbqB1gIEghYCG4IhAi2COh4nGNgZGBgUGYoZWBnAAEmIOYCQgaG/2A+AwAYCQG2AHicXZBNaoNAGIZfE5PQCKFQ2lUps2oXBfOzzAESyDKBQJdGR2NQR3QSSE/QE/QEPUUPUHqsvsrXjTMw83zPvPMNCuAWP3DQDAejdm1GjzwS7pMmwi75XngAD4/CQ/oX4TFe4Qt7uMMbOzjuDc0EmXCP/C7cJ38Iu+RP4QEe8CU8pP8WHmOPX2EPz87TPo202ey2OjlnQSXV/6arOjWFmvszMWtd6CqwOlKHq6ovycLaWMWVydXKFFZnmVFlZU46tP7R2nI5ncbi/dDkfDtFBA2DDXbYkhKc+V0Bqs5Zt9JM1HQGBRTm/EezTmZNKtpcAMs9Yu6AK9caF76zoLWIWcfMGOSkVduvSWechqZsz040Ib2PY3urxBJTzriT95lipz+TN1fmAAAAeJxtkMl2wjAMRfOAhABlKm2h80C3+ajgCKKDY6cegP59TYBzukAL+z1Zsq8ctaJTTKPrsUQLbXQQI0EXKXroY4AbDDHCGBNMcYsZ7nCPB8yxwCOe8IwXvOIN7/jAJ76wxHfUqWX+OzgumWAjJMV17i0Ndlr6irLKO+qftdT7i6y4uFSUvCknay+lFYZIZaQcmfH/xIFdYn98bqhra1aKTM/6lWMnyaYirx1rFUQZFBkb2zJUtoXeJCeg0WnLtHeSFc3OtrnozNwqi0TkSpBMDB1nSde5oJXW23hTS2/T0LilglXX7dmFVxLnq5U0vYATHFk3zX3BOisoQHNDFDeZnqKDy9hRNawN7Vh727hFzcJ5c8TILrKZfH7tIPxAFP0BpLeJPA==) format("woff");font-weight:400;font-style:normal}.video-js .vjs-big-play-button .vjs-icon-placeholder:before,.video-js .vjs-play-control .vjs-icon-placeholder,.vjs-icon-play{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-big-play-button .vjs-icon-placeholder:before,.video-js .vjs-play-control .vjs-icon-placeholder:before,.vjs-icon-play:before{content:"\f101"}.vjs-icon-play-circle{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-play-circle:before{content:"\f102"}.video-js .vjs-play-control.vjs-playing .vjs-icon-placeholder,.vjs-icon-pause{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-play-control.vjs-playing .vjs-icon-placeholder:before,.vjs-icon-pause:before{content:"\f103"}.video-js .vjs-mute-control.vjs-vol-0 .vjs-icon-placeholder,.vjs-icon-volume-mute{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-mute-control.vjs-vol-0 .vjs-icon-placeholder:before,.vjs-icon-volume-mute:before{content:"\f104"}.video-js .vjs-mute-control.vjs-vol-1 .vjs-icon-placeholder,.vjs-icon-volume-low{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-mute-control.vjs-vol-1 .vjs-icon-placeholder:before,.vjs-icon-volume-low:before{content:"\f105"}.video-js .vjs-mute-control.vjs-vol-2 .vjs-icon-placeholder,.vjs-icon-volume-mid{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-mute-control.vjs-vol-2 .vjs-icon-placeholder:before,.vjs-icon-volume-mid:before{content:"\f106"}.video-js .vjs-mute-control .vjs-icon-placeholder,.vjs-icon-volume-high{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-mute-control .vjs-icon-placeholder:before,.vjs-icon-volume-high:before{content:"\f107"}.video-js .vjs-fullscreen-control .vjs-icon-placeholder,.vjs-icon-fullscreen-enter{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-fullscreen-control .vjs-icon-placeholder:before,.vjs-icon-fullscreen-enter:before{content:"\f108"}.video-js.vjs-fullscreen .vjs-fullscreen-control .vjs-icon-placeholder,.vjs-icon-fullscreen-exit{font-family:VideoJS;font-weight:400;font-style:normal}.video-js.vjs-fullscreen .vjs-fullscreen-control .vjs-icon-placeholder:before,.vjs-icon-fullscreen-exit:before{content:"\f109"}.vjs-icon-square{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-square:before{content:"\f10a"}.vjs-icon-spinner{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-spinner:before{content:"\f10b"}.video-js .vjs-subs-caps-button .vjs-icon-placeholder,.video-js .vjs-subtitles-button .vjs-icon-placeholder,.video-js.video-js:lang(en-AU) .vjs-subs-caps-button .vjs-icon-placeholder,.video-js.video-js:lang(en-GB) .vjs-subs-caps-button .vjs-icon-placeholder,.video-js.video-js:lang(en-IE) .vjs-subs-caps-button .vjs-icon-placeholder,.video-js.video-js:lang(en-NZ) .vjs-subs-caps-button .vjs-icon-placeholder,.vjs-icon-subtitles{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-subs-caps-button .vjs-icon-placeholder:before,.video-js .vjs-subtitles-button .vjs-icon-placeholder:before,.video-js.video-js:lang(en-AU) .vjs-subs-caps-button .vjs-icon-placeholder:before,.video-js.video-js:lang(en-GB) .vjs-subs-caps-button .vjs-icon-placeholder:before,.video-js.video-js:lang(en-IE) .vjs-subs-caps-button .vjs-icon-placeholder:before,.video-js.video-js:lang(en-NZ) .vjs-subs-caps-button .vjs-icon-placeholder:before,.vjs-icon-subtitles:before{content:"\f10c"}.video-js .vjs-captions-button .vjs-icon-placeholder,.video-js:lang(en) .vjs-subs-caps-button .vjs-icon-placeholder,.video-js:lang(fr-CA) .vjs-subs-caps-button .vjs-icon-placeholder,.vjs-icon-captions{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-captions-button .vjs-icon-placeholder:before,.video-js:lang(en) .vjs-subs-caps-button .vjs-icon-placeholder:before,.video-js:lang(fr-CA) .vjs-subs-caps-button .vjs-icon-placeholder:before,.vjs-icon-captions:before{content:"\f10d"}.video-js .vjs-chapters-button .vjs-icon-placeholder,.vjs-icon-chapters{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-chapters-button .vjs-icon-placeholder:before,.vjs-icon-chapters:before{content:"\f10e"}.vjs-icon-share{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-share:before{content:"\f10f"}.vjs-icon-cog{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-cog:before{content:"\f110"}.video-js .vjs-play-progress,.video-js .vjs-volume-level,.vjs-icon-circle,.vjs-seek-to-live-control .vjs-icon-placeholder{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-play-progress:before,.video-js .vjs-volume-level:before,.vjs-icon-circle:before,.vjs-seek-to-live-control .vjs-icon-placeholder:before{content:"\f111"}.vjs-icon-circle-outline{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-circle-outline:before{content:"\f112"}.vjs-icon-circle-inner-circle{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-circle-inner-circle:before{content:"\f113"}.vjs-icon-hd{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-hd:before{content:"\f114"}.video-js .vjs-control.vjs-close-button .vjs-icon-placeholder,.vjs-icon-cancel{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-control.vjs-close-button .vjs-icon-placeholder:before,.vjs-icon-cancel:before{content:"\f115"}.video-js .vjs-play-control.vjs-ended .vjs-icon-placeholder,.vjs-icon-replay{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-play-control.vjs-ended .vjs-icon-placeholder:before,.vjs-icon-replay:before{content:"\f116"}.vjs-icon-facebook{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-facebook:before{content:"\f117"}.vjs-icon-gplus{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-gplus:before{content:"\f118"}.vjs-icon-linkedin{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-linkedin:before{content:"\f119"}.vjs-icon-twitter{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-twitter:before{content:"\f11a"}.vjs-icon-tumblr{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-tumblr:before{content:"\f11b"}.vjs-icon-pinterest{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-pinterest:before{content:"\f11c"}.video-js .vjs-descriptions-button .vjs-icon-placeholder,.vjs-icon-audio-description{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-descriptions-button .vjs-icon-placeholder:before,.vjs-icon-audio-description:before{content:"\f11d"}.video-js .vjs-audio-button .vjs-icon-placeholder,.vjs-icon-audio{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-audio-button .vjs-icon-placeholder:before,.vjs-icon-audio:before{content:"\f11e"}.vjs-icon-next-item{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-next-item:before{content:"\f11f"}.vjs-icon-previous-item{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-previous-item:before{content:"\f120"}.video-js .vjs-picture-in-picture-control .vjs-icon-placeholder,.vjs-icon-picture-in-picture-enter{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-picture-in-picture-control .vjs-icon-placeholder:before,.vjs-icon-picture-in-picture-enter:before{content:"\f121"}.video-js.vjs-picture-in-picture .vjs-picture-in-picture-control .vjs-icon-placeholder,.vjs-icon-picture-in-picture-exit{font-family:VideoJS;font-weight:400;font-style:normal}.video-js.vjs-picture-in-picture .vjs-picture-in-picture-control .vjs-icon-placeholder:before,.vjs-icon-picture-in-picture-exit:before{content:"\f122"}.video-js{display:block;vertical-align:top;box-sizing:border-box;color:#fff;background-color:#000;position:relative;padding:0;font-size:10px;line-height:1;font-weight:400;font-style:normal;font-family:Arial,Helvetica,sans-serif;word-break:initial}.video-js:-moz-full-screen{position:absolute}.video-js:-webkit-full-screen{width:100%!important;height:100%!important}.video-js[tabindex="-1"]{outline:0}.video-js *,.video-js :after,.video-js :before{box-sizing:inherit}.video-js ul{font-family:inherit;font-size:inherit;line-height:inherit;list-style-position:outside;margin-left:0;margin-right:0;margin-top:0;margin-bottom:0}.video-js.vjs-16-9,.video-js.vjs-4-3,.video-js.vjs-fluid{width:100%;max-width:100%;height:0}.video-js.vjs-16-9{padding-top:56.25%}.video-js.vjs-4-3{padding-top:75%}.video-js.vjs-fill{width:100%;height:100%}.video-js .vjs-tech{position:absolute;top:0;left:0;width:100%;height:100%}body.vjs-full-window{padding:0;margin:0;height:100%}.vjs-full-window .video-js.vjs-fullscreen{position:fixed;overflow:hidden;z-index:1000;left:0;top:0;bottom:0;right:0}.video-js.vjs-fullscreen{width:100%!important;height:100%!important;padding-top:0!important}.video-js.vjs-fullscreen.vjs-user-inactive{cursor:none}.vjs-hidden{display:none!important}.vjs-disabled{opacity:.5;cursor:default}.video-js .vjs-offscreen{height:1px;left:-9999px;position:absolute;top:0;width:1px}.vjs-lock-showing{display:block!important;opacity:1;visibility:visible}.vjs-no-js{padding:20px;color:#fff;background-color:#000;font-size:18px;font-family:Arial,Helvetica,sans-serif;text-align:center;width:300px;height:150px;margin:0 auto}.vjs-no-js a,.vjs-no-js a:visited{color:#66a8cc}.video-js .vjs-big-play-button{font-size:3em;line-height:1.5em;height:1.63332em;width:3em;display:block;position:absolute;top:10px;left:10px;padding:0;cursor:pointer;opacity:1;border:.06666em solid #fff;background-color:#2b333f;background-color:rgba(43,51,63,.7);border-radius:.3em;transition:all .4s}.vjs-big-play-centered .vjs-big-play-button{top:50%;left:50%;margin-top:-.81666em;margin-left:-1.5em}.video-js .vjs-big-play-button:focus,.video-js:hover .vjs-big-play-button{border-color:#fff;background-color:#73859f;background-color:rgba(115,133,159,.5);transition:all 0s}.vjs-controls-disabled .vjs-big-play-button,.vjs-error .vjs-big-play-button,.vjs-has-started .vjs-big-play-button,.vjs-using-native-controls .vjs-big-play-button{display:none}.vjs-has-started.vjs-paused.vjs-show-big-play-button-on-pause .vjs-big-play-button{display:block}.video-js button{background:0 0;border:none;color:inherit;display:inline-block;font-size:inherit;line-height:inherit;text-transform:none;text-decoration:none;transition:none;-webkit-appearance:none;-moz-appearance:none;appearance:none}.vjs-control .vjs-button{width:100%;height:100%}.video-js .vjs-control.vjs-close-button{cursor:pointer;height:3em;position:absolute;right:0;top:.5em;z-index:2}.video-js .vjs-modal-dialog{background:rgba(0,0,0,.8);background:linear-gradient(180deg,rgba(0,0,0,.8),rgba(255,255,255,0));overflow:auto}.video-js .vjs-modal-dialog>*{box-sizing:border-box}.vjs-modal-dialog .vjs-modal-dialog-content{font-size:1.2em;line-height:1.5;padding:20px 24px;z-index:1}.vjs-menu-button{cursor:pointer}.vjs-menu-button.vjs-disabled{cursor:default}.vjs-workinghover .vjs-menu-button.vjs-disabled:hover .vjs-menu{display:none}.vjs-menu .vjs-menu-content{display:block;padding:0;margin:0;font-family:Arial,Helvetica,sans-serif;overflow:auto}.vjs-menu .vjs-menu-content>*{box-sizing:border-box}.vjs-scrubbing .vjs-control.vjs-menu-button:hover .vjs-menu{display:none}.vjs-menu li{list-style:none;margin:0;padding:.2em 0;line-height:1.4em;font-size:1.2em;text-align:center;text-transform:lowercase}.js-focus-visible .vjs-menu li.vjs-menu-item:hover,.vjs-menu li.vjs-menu-item:focus,.vjs-menu li.vjs-menu-item:hover{background-color:#73859f;background-color:rgba(115,133,159,.5)}.js-focus-visible .vjs-menu li.vjs-selected:hover,.vjs-menu li.vjs-selected,.vjs-menu li.vjs-selected:focus,.vjs-menu li.vjs-selected:hover{background-color:#fff;color:#2b333f}.vjs-menu li.vjs-menu-title{text-align:center;text-transform:uppercase;font-size:1em;line-height:2em;padding:0;margin:0 0 .3em 0;font-weight:700;cursor:default}.vjs-menu-button-popup .vjs-menu{display:none;position:absolute;bottom:0;width:10em;left:-3em;height:0;margin-bottom:1.5em;border-top-color:rgba(43,51,63,.7)}.vjs-menu-button-popup .vjs-menu .vjs-menu-content{background-color:#2b333f;background-color:rgba(43,51,63,.7);position:absolute;width:100%;bottom:1.5em;max-height:15em}.vjs-layout-tiny .vjs-menu-button-popup .vjs-menu .vjs-menu-content,.vjs-layout-x-small .vjs-menu-button-popup .vjs-menu .vjs-menu-content{max-height:5em}.vjs-layout-small .vjs-menu-button-popup .vjs-menu .vjs-menu-content{max-height:10em}.vjs-layout-medium .vjs-menu-button-popup .vjs-menu .vjs-menu-content{max-height:14em}.vjs-layout-huge .vjs-menu-button-popup .vjs-menu .vjs-menu-content,.vjs-layout-large .vjs-menu-button-popup .vjs-menu .vjs-menu-content,.vjs-layout-x-large .vjs-menu-button-popup .vjs-menu .vjs-menu-content{max-height:25em}.vjs-menu-button-popup .vjs-menu.vjs-lock-showing,.vjs-workinghover .vjs-menu-button-popup:hover .vjs-menu{display:block}.video-js .vjs-menu-button-inline{transition:all .4s;overflow:hidden}.video-js .vjs-menu-button-inline:before{width:2.222222222em}.video-js .vjs-menu-button-inline.vjs-slider-active,.video-js .vjs-menu-button-inline:focus,.video-js .vjs-menu-button-inline:hover,.video-js.vjs-no-flex .vjs-menu-button-inline{width:12em}.vjs-menu-button-inline .vjs-menu{opacity:0;height:100%;width:auto;position:absolute;left:4em;top:0;padding:0;margin:0;transition:all .4s}.vjs-menu-button-inline.vjs-slider-active .vjs-menu,.vjs-menu-button-inline:focus .vjs-menu,.vjs-menu-button-inline:hover .vjs-menu{display:block;opacity:1}.vjs-no-flex .vjs-menu-button-inline .vjs-menu{display:block;opacity:1;position:relative;width:auto}.vjs-no-flex .vjs-menu-button-inline.vjs-slider-active .vjs-menu,.vjs-no-flex .vjs-menu-button-inline:focus .vjs-menu,.vjs-no-flex .vjs-menu-button-inline:hover .vjs-menu{width:auto}.vjs-menu-button-inline .vjs-menu-content{width:auto;height:100%;margin:0;overflow:hidden}.video-js .vjs-control-bar{display:none;width:100%;position:absolute;bottom:0;left:0;right:0;height:3em;background-color:#2b333f;background-color:rgba(43,51,63,.7)}.vjs-has-started .vjs-control-bar{display:flex;visibility:visible;opacity:1;transition:visibility .1s,opacity .1s}.vjs-has-started.vjs-user-inactive.vjs-playing .vjs-control-bar{visibility:visible;opacity:0;transition:visibility 1s,opacity 1s}.vjs-controls-disabled .vjs-control-bar,.vjs-error .vjs-control-bar,.vjs-using-native-controls .vjs-control-bar{display:none!important}.vjs-audio.vjs-has-started.vjs-user-inactive.vjs-playing .vjs-control-bar{opacity:1;visibility:visible}.vjs-has-started.vjs-no-flex .vjs-control-bar{display:table}.video-js .vjs-control{position:relative;text-align:center;margin:0;padding:0;height:100%;width:4em;flex:none}.vjs-button>.vjs-icon-placeholder:before{font-size:1.8em;line-height:1.67}.video-js .vjs-control:focus,.video-js .vjs-control:focus:before,.video-js .vjs-control:hover:before{text-shadow:0 0 1em #fff}.video-js .vjs-control-text{border:0;clip:rect(0 0 0 0);height:1px;overflow:hidden;padding:0;position:absolute;width:1px}.vjs-no-flex .vjs-control{display:table-cell;vertical-align:middle}.video-js .vjs-custom-control-spacer{display:none}.video-js .vjs-progress-control{cursor:pointer;flex:auto;display:flex;align-items:center;min-width:4em;touch-action:none}.video-js .vjs-progress-control.disabled{cursor:default}.vjs-live .vjs-progress-control{display:none}.vjs-liveui .vjs-progress-control{display:flex;align-items:center}.vjs-no-flex .vjs-progress-control{width:auto}.video-js .vjs-progress-holder{flex:auto;transition:all .2s;height:.3em}.video-js .vjs-progress-control .vjs-progress-holder{margin:0 10px}.video-js .vjs-progress-control:hover .vjs-progress-holder{font-size:1.6666666667em}.video-js .vjs-progress-control:hover .vjs-progress-holder.disabled{font-size:1em}.video-js .vjs-progress-holder .vjs-load-progress,.video-js .vjs-progress-holder .vjs-load-progress div,.video-js .vjs-progress-holder .vjs-play-progress{position:absolute;display:block;height:100%;margin:0;padding:0;width:0}.video-js .vjs-play-progress{background-color:#fff}.video-js .vjs-play-progress:before{font-size:.9em;position:absolute;right:-.5em;top:-.3333333333em;z-index:1}.video-js .vjs-load-progress{background:rgba(115,133,159,.5)}.video-js .vjs-load-progress div{background:rgba(115,133,159,.75)}.video-js .vjs-time-tooltip{background-color:#fff;background-color:rgba(255,255,255,.8);border-radius:.3em;color:#000;float:right;font-family:Arial,Helvetica,sans-serif;font-size:1em;padding:6px 8px 8px 8px;pointer-events:none;position:absolute;top:-3.4em;visibility:hidden;z-index:1}.video-js .vjs-progress-holder:focus .vjs-time-tooltip{display:none}.video-js .vjs-progress-control:hover .vjs-progress-holder:focus .vjs-time-tooltip,.video-js .vjs-progress-control:hover .vjs-time-tooltip{display:block;font-size:.6em;visibility:visible}.video-js .vjs-progress-control.disabled:hover .vjs-time-tooltip{font-size:1em}.video-js .vjs-progress-control .vjs-mouse-display{display:none;position:absolute;width:1px;height:100%;background-color:#000;z-index:1}.vjs-no-flex .vjs-progress-control .vjs-mouse-display{z-index:0}.video-js .vjs-progress-control:hover .vjs-mouse-display{display:block}.video-js.vjs-user-inactive .vjs-progress-control .vjs-mouse-display{visibility:hidden;opacity:0;transition:visibility 1s,opacity 1s}.video-js.vjs-user-inactive.vjs-no-flex .vjs-progress-control .vjs-mouse-display{display:none}.vjs-mouse-display .vjs-time-tooltip{color:#fff;background-color:#000;background-color:rgba(0,0,0,.8)}.video-js .vjs-slider{position:relative;cursor:pointer;padding:0;margin:0 .45em 0 .45em;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:#73859f;background-color:rgba(115,133,159,.5)}.video-js .vjs-slider.disabled{cursor:default}.video-js .vjs-slider:focus{text-shadow:0 0 1em #fff;box-shadow:0 0 1em #fff}.video-js .vjs-mute-control{cursor:pointer;flex:none}.video-js .vjs-volume-control{cursor:pointer;margin-right:1em;display:flex}.video-js .vjs-volume-control.vjs-volume-horizontal{width:5em}.video-js .vjs-volume-panel .vjs-volume-control{visibility:visible;opacity:0;width:1px;height:1px;margin-left:-1px}.video-js .vjs-volume-panel{transition:width 1s}.video-js .vjs-volume-panel .vjs-mute-control:hover~.vjs-volume-control,.video-js .vjs-volume-panel .vjs-volume-control.vjs-slider-active,.video-js .vjs-volume-panel .vjs-volume-control:active,.video-js .vjs-volume-panel .vjs-volume-control:hover,.video-js .vjs-volume-panel:active .vjs-volume-control,.video-js .vjs-volume-panel:focus .vjs-volume-control,.video-js .vjs-volume-panel:hover .vjs-volume-control{visibility:visible;opacity:1;position:relative;transition:visibility .1s,opacity .1s,height .1s,width .1s,left 0s,top 0s}.video-js .vjs-volume-panel .vjs-mute-control:hover~.vjs-volume-control.vjs-volume-horizontal,.video-js .vjs-volume-panel .vjs-volume-control.vjs-slider-active.vjs-volume-horizontal,.video-js .vjs-volume-panel .vjs-volume-control:active.vjs-volume-horizontal,.video-js .vjs-volume-panel .vjs-volume-control:hover.vjs-volume-horizontal,.video-js .vjs-volume-panel:active .vjs-volume-control.vjs-volume-horizontal,.video-js .vjs-volume-panel:focus .vjs-volume-control.vjs-volume-horizontal,.video-js .vjs-volume-panel:hover .vjs-volume-control.vjs-volume-horizontal{width:5em;height:3em}.video-js .vjs-volume-panel .vjs-mute-control:hover~.vjs-volume-control.vjs-volume-vertical,.video-js .vjs-volume-panel .vjs-volume-control.vjs-slider-active.vjs-volume-vertical,.video-js .vjs-volume-panel .vjs-volume-control:active.vjs-volume-vertical,.video-js .vjs-volume-panel .vjs-volume-control:hover.vjs-volume-vertical,.video-js .vjs-volume-panel:active .vjs-volume-control.vjs-volume-vertical,.video-js .vjs-volume-panel:focus .vjs-volume-control.vjs-volume-vertical,.video-js .vjs-volume-panel:hover .vjs-volume-control.vjs-volume-vertical{left:-3.5em}.video-js .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-slider-active,.video-js .vjs-volume-panel.vjs-volume-panel-horizontal:active,.video-js .vjs-volume-panel.vjs-volume-panel-horizontal:hover{width:9em;transition:width .1s}.video-js .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-mute-toggle-only{width:4em}.video-js .vjs-volume-panel .vjs-volume-control.vjs-volume-vertical{height:8em;width:3em;left:-3000em;transition:visibility 1s,opacity 1s,height 1s 1s,width 1s 1s,left 1s 1s,top 1s 1s}.video-js .vjs-volume-panel .vjs-volume-control.vjs-volume-horizontal{transition:visibility 1s,opacity 1s,height 1s 1s,width 1s,left 1s 1s,top 1s 1s}.video-js.vjs-no-flex .vjs-volume-panel .vjs-volume-control.vjs-volume-horizontal{width:5em;height:3em;visibility:visible;opacity:1;position:relative;transition:none}.video-js.vjs-no-flex .vjs-volume-control.vjs-volume-vertical,.video-js.vjs-no-flex .vjs-volume-panel .vjs-volume-control.vjs-volume-vertical{position:absolute;bottom:3em;left:.5em}.video-js .vjs-volume-panel{display:flex}.video-js .vjs-volume-bar{margin:1.35em .45em}.vjs-volume-bar.vjs-slider-horizontal{width:5em;height:.3em}.vjs-volume-bar.vjs-slider-vertical{width:.3em;height:5em;margin:1.35em auto}.video-js .vjs-volume-level{position:absolute;bottom:0;left:0;background-color:#fff}.video-js .vjs-volume-level:before{position:absolute;font-size:.9em}.vjs-slider-vertical .vjs-volume-level{width:.3em}.vjs-slider-vertical .vjs-volume-level:before{top:-.5em;left:-.3em}.vjs-slider-horizontal .vjs-volume-level{height:.3em}.vjs-slider-horizontal .vjs-volume-level:before{top:-.3em;right:-.5em}.video-js .vjs-volume-panel.vjs-volume-panel-vertical{width:4em}.vjs-volume-bar.vjs-slider-vertical .vjs-volume-level{height:100%}.vjs-volume-bar.vjs-slider-horizontal .vjs-volume-level{width:100%}.video-js .vjs-volume-vertical{width:3em;height:8em;bottom:8em;background-color:#2b333f;background-color:rgba(43,51,63,.7)}.video-js .vjs-volume-horizontal .vjs-menu{left:-2em}.vjs-poster{display:inline-block;vertical-align:middle;background-repeat:no-repeat;background-position:50% 50%;background-size:contain;background-color:#000;cursor:pointer;margin:0;padding:0;position:absolute;top:0;right:0;bottom:0;left:0;height:100%}.vjs-has-started .vjs-poster{display:none}.vjs-audio.vjs-has-started .vjs-poster{display:block}.vjs-using-native-controls .vjs-poster{display:none}.video-js .vjs-live-control{display:flex;align-items:flex-start;flex:auto;font-size:1em;line-height:3em}.vjs-no-flex .vjs-live-control{display:table-cell;width:auto;text-align:left}.video-js.vjs-liveui .vjs-live-control,.video-js:not(.vjs-live) .vjs-live-control{display:none}.video-js .vjs-seek-to-live-control{cursor:pointer;flex:none;display:inline-flex;height:100%;padding-left:.5em;padding-right:.5em;font-size:1em;line-height:3em;width:auto;min-width:4em}.vjs-no-flex .vjs-seek-to-live-control{display:table-cell;width:auto;text-align:left}.video-js.vjs-live:not(.vjs-liveui) .vjs-seek-to-live-control,.video-js:not(.vjs-live) .vjs-seek-to-live-control{display:none}.vjs-seek-to-live-control.vjs-control.vjs-at-live-edge{cursor:auto}.vjs-seek-to-live-control .vjs-icon-placeholder{margin-right:.5em;color:#888}.vjs-seek-to-live-control.vjs-control.vjs-at-live-edge .vjs-icon-placeholder{color:red}.video-js .vjs-time-control{flex:none;font-size:1em;line-height:3em;min-width:2em;width:auto;padding-left:1em;padding-right:1em}.vjs-live .vjs-time-control{display:none}.video-js .vjs-current-time,.vjs-no-flex .vjs-current-time{display:none}.video-js .vjs-duration,.vjs-no-flex .vjs-duration{display:none}.vjs-time-divider{display:none;line-height:3em}.vjs-live .vjs-time-divider{display:none}.video-js .vjs-play-control{cursor:pointer}.video-js .vjs-play-control .vjs-icon-placeholder{flex:none}.vjs-text-track-display{position:absolute;bottom:3em;left:0;right:0;top:0;pointer-events:none}.video-js.vjs-user-inactive.vjs-playing .vjs-text-track-display{bottom:1em}.video-js .vjs-text-track{font-size:1.4em;text-align:center;margin-bottom:.1em}.vjs-subtitles{color:#fff}.vjs-captions{color:#fc6}.vjs-tt-cue{display:block}video::-webkit-media-text-track-display{transform:translateY(-3em)}.video-js.vjs-user-inactive.vjs-playing video::-webkit-media-text-track-display{transform:translateY(-1.5em)}.video-js .vjs-picture-in-picture-control{cursor:pointer;flex:none}.video-js .vjs-fullscreen-control{cursor:pointer;flex:none}.vjs-playback-rate .vjs-playback-rate-value,.vjs-playback-rate>.vjs-menu-button{position:absolute;top:0;left:0;width:100%;height:100%}.vjs-playback-rate .vjs-playback-rate-value{pointer-events:none;font-size:1.5em;line-height:2;text-align:center}.vjs-playback-rate .vjs-menu{width:4em;left:0}.vjs-error .vjs-error-display .vjs-modal-dialog-content{font-size:1.4em;text-align:center}.vjs-error .vjs-error-display:before{color:#fff;content:"X";font-family:Arial,Helvetica,sans-serif;font-size:4em;left:0;line-height:1;margin-top:-.5em;position:absolute;text-shadow:.05em .05em .1em #000;text-align:center;top:50%;vertical-align:middle;width:100%}.vjs-loading-spinner{display:none;position:absolute;top:50%;left:50%;margin:-25px 0 0 -25px;opacity:.85;text-align:left;border:6px solid rgba(43,51,63,.7);box-sizing:border-box;background-clip:padding-box;width:50px;height:50px;border-radius:25px;visibility:hidden}.vjs-seeking .vjs-loading-spinner,.vjs-waiting .vjs-loading-spinner{display:block;-webkit-animation:vjs-spinner-show 0s linear .3s forwards;animation:vjs-spinner-show 0s linear .3s forwards}.vjs-loading-spinner:after,.vjs-loading-spinner:before{content:"";position:absolute;margin:-6px;box-sizing:inherit;width:inherit;height:inherit;border-radius:inherit;opacity:1;border:inherit;border-color:transparent;border-top-color:#fff}.vjs-seeking .vjs-loading-spinner:after,.vjs-seeking .vjs-loading-spinner:before,.vjs-waiting .vjs-loading-spinner:after,.vjs-waiting .vjs-loading-spinner:before{-webkit-animation:vjs-spinner-spin 1.1s cubic-bezier(.6,.2,0,.8) infinite,vjs-spinner-fade 1.1s linear infinite;animation:vjs-spinner-spin 1.1s cubic-bezier(.6,.2,0,.8) infinite,vjs-spinner-fade 1.1s linear infinite}.vjs-seeking .vjs-loading-spinner:before,.vjs-waiting .vjs-loading-spinner:before{border-top-color:#fff}.vjs-seeking .vjs-loading-spinner:after,.vjs-waiting .vjs-loading-spinner:after{border-top-color:#fff;-webkit-animation-delay:.44s;animation-delay:.44s}@keyframes vjs-spinner-show{to{visibility:visible}}@-webkit-keyframes vjs-spinner-show{to{visibility:visible}}@keyframes vjs-spinner-spin{100%{transform:rotate(360deg)}}@-webkit-keyframes vjs-spinner-spin{100%{-webkit-transform:rotate(360deg)}}@keyframes vjs-spinner-fade{0%{border-top-color:#73859f}20%{border-top-color:#73859f}35%{border-top-color:#fff}60%{border-top-color:#73859f}100%{border-top-color:#73859f}}@-webkit-keyframes vjs-spinner-fade{0%{border-top-color:#73859f}20%{border-top-color:#73859f}35%{border-top-color:#fff}60%{border-top-color:#73859f}100%{border-top-color:#73859f}}.vjs-chapters-button .vjs-menu ul{width:24em}.video-js .vjs-subs-caps-button+.vjs-menu .vjs-captions-menu-item .vjs-menu-item-text .vjs-icon-placeholder{vertical-align:middle;display:inline-block;margin-bottom:-.1em}.video-js .vjs-subs-caps-button+.vjs-menu .vjs-captions-menu-item .vjs-menu-item-text .vjs-icon-placeholder:before{font-family:VideoJS;content:"";font-size:1.5em;line-height:inherit}.video-js .vjs-audio-button+.vjs-menu .vjs-main-desc-menu-item .vjs-menu-item-text .vjs-icon-placeholder{vertical-align:middle;display:inline-block;margin-bottom:-.1em}.video-js .vjs-audio-button+.vjs-menu .vjs-main-desc-menu-item .vjs-menu-item-text .vjs-icon-placeholder:before{font-family:VideoJS;content:" ";font-size:1.5em;line-height:inherit}.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-audio-button,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-captions-button,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-chapters-button,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-current-time,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-descriptions-button,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-duration,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-playback-rate,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-remaining-time,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-subtitles-button,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-time-divider,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-volume-control,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-audio-button,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-captions-button,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-chapters-button,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-current-time,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-descriptions-button,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-duration,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-playback-rate,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-remaining-time,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-subtitles-button,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-time-divider,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-volume-control,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-audio-button,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-captions-button,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-chapters-button,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-current-time,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-descriptions-button,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-duration,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-playback-rate,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-remaining-time,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-subtitles-button,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-time-divider,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-volume-control{display:none}.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-slider-active,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-volume-panel.vjs-volume-panel-horizontal:active,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-volume-panel.vjs-volume-panel-horizontal:hover,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-slider-active,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-volume-panel.vjs-volume-panel-horizontal:active,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-volume-panel.vjs-volume-panel-horizontal:hover,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-slider-active,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-volume-panel.vjs-volume-panel-horizontal:active,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-volume-panel.vjs-volume-panel-horizontal:hover{width:auto;width:initial}.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-subs-caps-button,.video-js:not(.vjs-fullscreen).vjs-layout-x-small:not(.vjs-live) .vjs-subs-caps-button,.video-js:not(.vjs-fullscreen).vjs-layout-x-small:not(.vjs-liveui) .vjs-subs-caps-button{display:none}.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-custom-control-spacer,.video-js:not(.vjs-fullscreen).vjs-layout-x-small.vjs-liveui .vjs-custom-control-spacer{flex:auto;display:block}.video-js:not(.vjs-fullscreen).vjs-layout-tiny.vjs-no-flex .vjs-custom-control-spacer,.video-js:not(.vjs-fullscreen).vjs-layout-x-small.vjs-liveui.vjs-no-flex .vjs-custom-control-spacer{width:auto}.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-progress-control,.video-js:not(.vjs-fullscreen).vjs-layout-x-small.vjs-liveui .vjs-progress-control{display:none}.vjs-modal-dialog.vjs-text-track-settings{background-color:#2b333f;background-color:rgba(43,51,63,.75);color:#fff;height:70%}.vjs-text-track-settings .vjs-modal-dialog-content{display:table}.vjs-text-track-settings .vjs-track-settings-colors,.vjs-text-track-settings .vjs-track-settings-controls,.vjs-text-track-settings .vjs-track-settings-font{display:table-cell}.vjs-text-track-settings .vjs-track-settings-controls{text-align:right;vertical-align:bottom}@supports (display:grid){.vjs-text-track-settings .vjs-modal-dialog-content{display:grid;grid-template-columns:1fr 1fr;grid-template-rows:1fr;padding:20px 24px 0 24px}.vjs-track-settings-controls .vjs-default-button{margin-bottom:20px}.vjs-text-track-settings .vjs-track-settings-controls{grid-column:1/-1}.vjs-layout-small .vjs-text-track-settings .vjs-modal-dialog-content,.vjs-layout-tiny .vjs-text-track-settings .vjs-modal-dialog-content,.vjs-layout-x-small .vjs-text-track-settings .vjs-modal-dialog-content{grid-template-columns:1fr}}.vjs-track-setting>select{margin-right:1em;margin-bottom:.5em}.vjs-text-track-settings fieldset{margin:5px;padding:3px;border:none}.vjs-text-track-settings fieldset span{display:inline-block}.vjs-text-track-settings fieldset span>select{max-width:7.3em}.vjs-text-track-settings legend{color:#fff;margin:0 0 5px 0}.vjs-text-track-settings .vjs-label{position:absolute;clip:rect(1px 1px 1px 1px);clip:rect(1px,1px,1px,1px);display:block;margin:0 0 5px 0;padding:0;border:0;height:1px;width:1px;overflow:hidden}.vjs-track-settings-controls button:active,.vjs-track-settings-controls button:focus{outline-style:solid;outline-width:medium;background-image:linear-gradient(0deg,#fff 88%,#73859f 100%)}.vjs-track-settings-controls button:hover{color:rgba(43,51,63,.75)}.vjs-track-settings-controls button{background-color:#fff;background-image:linear-gradient(-180deg,#fff 88%,#73859f 100%);color:#2b333f;cursor:pointer;border-radius:2px}.vjs-track-settings-controls .vjs-default-button{margin-right:1em}@media print{.video-js>:not(.vjs-tech):not(.vjs-poster){visibility:hidden}}.vjs-resize-manager{position:absolute;top:0;left:0;width:100%;height:100%;border:none;z-index:-1000}.js-focus-visible .video-js :focus:not(.focus-visible){outline:0;background:0 0}.video-js .vjs-menu :focus:not(:focus-visible),.video-js :focus:not(:focus-visible){outline:0;background:0 0} \ No newline at end of file +@charset "UTF-8";.video-js .vjs-big-play-button .vjs-icon-placeholder:before,.video-js .vjs-modal-dialog,.vjs-button>.vjs-icon-placeholder:before,.vjs-modal-dialog .vjs-modal-dialog-content{position:absolute;top:0;left:0;width:100%;height:100%}.video-js .vjs-big-play-button .vjs-icon-placeholder:before,.vjs-button>.vjs-icon-placeholder:before{text-align:center}@font-face{font-family:VideoJS;src:url(data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAABDkAAsAAAAAG6gAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABCAAAADsAAABUIIslek9TLzIAAAFEAAAAPgAAAFZRiV3hY21hcAAAAYQAAADaAAADPv749/pnbHlmAAACYAAAC3AAABHQZg6OcWhlYWQAAA3QAAAAKwAAADYZw251aGhlYQAADfwAAAAdAAAAJA+RCLFobXR4AAAOHAAAABMAAACM744AAGxvY2EAAA4wAAAASAAAAEhF6kqubWF4cAAADngAAAAfAAAAIAE0AIFuYW1lAAAOmAAAASUAAAIK1cf1oHBvc3QAAA/AAAABJAAAAdPExYuNeJxjYGRgYOBiMGCwY2BycfMJYeDLSSzJY5BiYGGAAJA8MpsxJzM9kYEDxgPKsYBpDiBmg4gCACY7BUgAeJxjYGS7wTiBgZWBgaWQ5RkDA8MvCM0cwxDOeI6BgYmBlZkBKwhIc01hcPjI+FGJHcRdyA4RZgQRADK3CxEAAHic7dFZbsMgAEXRS0ycyZnnOeG7y+qC8pU1dHusIOXxuoxaOlwZYWQB0Aea4quIEN4E9LzKbKjzDeM6H/mua6Lmc/p8yhg0lvdYx15ZG8uOLQOGjMp3EzqmzJizYMmKNRu27Nhz4MiJMxeu3Ljz4Ekqm7T8P52G8PP3lnTOVk++Z6iN6QZzNN1F7ptuN7eGOjDUoaGODHVsuvU8MdTO9Hd5aqgzQ50b6sJQl4a6MtS1oW4MdWuoO0PdG+rBUI+GejLUs6FeDPVqqDdDvRvqw1CfhpqM9At0iFLaAAB4nJ1YDXBTVRZ+5/22TUlJ8we0pHlJm7RJf5O8F2j6EymlSPkpxaL8U2xpa3DKj0CBhc2IW4eWKSokIoLsuMqssM64f+jA4HSdWXXXscBq67IOs3FXZ1ZYWVyRFdo899yXtIBQZ90k7717zz3v3HPPOfd854YCCj9cL9dL0RQFOqCbGJnrHb5EayiKIWN8iA/hWBblo6hUWm8TtCDwE80WMJus/irwyxOdxeB0MDb14VNJHnXYoLLSl6FfCUYO9nYPTA8Epg9090LprfbBbZ2hY0UlJUXHQp3/vtWkS6EBv8+rPMq5u9692f/dNxJNiqwC1xPE9TCUgCsSdQWgE3XQD25lkG4CN2xmTcOXWBOyser6RN6KnGbKSbmQ3+d0OI1m2W8QzLLkI2sykrWAgJJEtA8vGGW/2Q+CmT3n8zS9wZwu2DCvtuZKZN3xkrLh36yCZuUomQSqGpY8t/25VfHVhw8z4ebGBtfLb0ya9PCaDc+8dGTvk2dsh6z7WzvowlXKUSWo9MJ15a3KrEP2loOr2Ojhw6iW6hf2BDdEccQvZGpaAy7YovSwq8kr7HGllxpd71rkS6G0Sf11sl9OvMK1+jwPPODxjUwkOim9CU3ix1wNjXDfmJSEn618Bs6lpWwUpU+8PCqLMY650zjq8VhCIP17NEKTx3eaLL+s5Pi6yJWaWjTHLR1jYzPSV9VF/6Ojdb/1kO3Mk3uhHC0x6gc1BjlKQ+nQFxTYdaJkZ7ySVxLBbhR1dsboNXp1tCYKW2LRaEzpYcIx2BKNxaL0ZaUnSqfFoiNhHKR/GkX6PWUSAaJelQaqZL1EpoHNsajSEyPSoJ9IjhIxTdjHLmwZvhRDOiFTY/YeQnvrVZmiTQtGncECXtFTBZLOVwwMRgoXHAkXzMzPn1nAJJ8jYSbMDaqN2waGLzNhih/bZynUBMpIWSg7VYi7DRx2m8ALkIdRCJwI6ArJx2EI8kaDWeTQKeAFk9fjl/1AvwktjQ1P7NjyMGQyfd4vjipX6M/i52D7Cq80kqlcxEcGXRr/FEcgs0u5uGgB4VWuMFfpdn2Re6Hi3PqzmxWKsz6+ae2Pn9hXXw/fqM859UiGC0oKYYILJBqJrsn1Z1E5qOs9rQCiUQRREjm8yJcbHF5cUJufX1vAHlefw0XgUoboS3ETfQlTxBC4SOtuE8VPRJTBSCQSjZCpk7Gqzu+masaZ2y7Zjehho4F3g82BNDkAHpORG4+OCS+f6JTPmtRn/PH1kch6d04sp7AQb25aQ/pqUyXeQ8vrebG8OYQdXOQ+585u0sdW9rqalzRURiJ+9F4MweRFrKUjl1GUYhH1A27WOHw5cTFSFPMo9EeUIGnQTZHIaJ7AHLaOKsOODaNF9jkBjYG2QEsQ2xjMUAx2bBEbeTBWMHwskBjngq56S/yfgkBnWBa4K9sqKtq2t1UI8S9He5XuBRbawAdatrQEAi30Aks2+LM8WeCbalVZkWNylvJ+dqJnzVb+OHlSoKW8nPCP7Rd+CcZ2DdWAGqJ2CBFOphgywFFCFBNtfAbGtNPBCwxvygHeYMZMY9ZboBqwq/pVrsbgN5tkv152ODlbMfiqwGMBgxa4Exz3QhovRIUp6acqZmQzRq0ypDXS2TPLT02YIkQETnOE445oOGxOmXAqUJNNG7XgupMjPq2ua9asrj5yY/yuKteO1Kx0YNJTufrirLe1mZnat7OL6rnUdCWenpW6I8mAnbsY8KWs1PuSovCW9A/Z25PQ24a7cNOqgmTkLmBMgh4THgc4b9k2IVv1/g/F5nGljwPLfOgHAzJzh45V/4+WenTzmMtR5Z7us2Tys909UHqrPY7KbckoxRvRHhmVc3cJGE97uml0R1S0jdULVl7EvZtDFVBF35N9cEdjpgmAiOlFZ+Dtoh93+D3zzHr8RRNZQhnCNMNbcegOvpEwZoL+06cJQ07h+th3fZ/7PVbVC6ngTAV/KoLFuO6+2KFcU651gEb5ugPSIb1D+Xp8V4+k3sEIGnw5mYe4If4k1lFYr6SCzmM2EQ8iWtmwjnBI9kTwe1TlfAmXh7H02by9fW2gsjKwtv0aaURKil4OdV7rDL1MXIFNrhdxohcZXYTnq47WisrKitaObbf5+yvkLi5J6lCNZZ+B6GC38VNBZBDidSS/+mSvh6s+srgC8pyKMvDtt+de3c9fU76ZPfuM8ud4Kv0fyP/LqfepMT/3oZxSqpZaTa1DaQYLY8TFsHYbWYsPoRhRWfL5eSSQbhUGgGC3YLbVMk6PitTFNGpAsNrC6D1VNBKgBHMejaiuRWEWGgsSDBTJjqWIl8kJLlsaLJ2tXDr6xGfT85bM2Q06a46x2HTgvdnV8z5YDy/27J4zt6x2VtkzjoYpkq36kaBr4eQSg7tyiVweWubXZugtadl58ydapfbORfKsDTuZ0OBgx4cfdjCf5tbWNITnL120fdOi1RV1C3uKGzNdwYLcMvZ3BxoPyTOCD1XvXTp7U10gWCVmTV9b3r2z0SkGWovb2hp9I89O8a2smlyaO8muMU+dRmtzp60IzAoFpjLr1n388boLyf0dRvxhsHZ0qbWqDkwqvvpkj4l0fY6EIXRi5sQSrAvsVYwXRy4qJ2EVtD1AN7a0HWth9ymvL1xc3WTUKK/TAHA/bXDVtVWfOMfuGxGZv4Ln/jVr9jc3j1yMv0tndmyt9Vq88Y9gH1wtLX3KWjot5++jWHgAoZZkQ14wGQ20Fli71UmKJAy4xKMSTGbVdybW7FDDAut9XpD5AzWrYO7zQ8qffqF8+Ynd/clrHcdyxGy3a/3+mfNnzC/cBsveTjnTvXf1o6vzOlZw7WtqtdmPK/Errz/6NNtD72zmNOZfbmYdTGHfoofqI79Oc+R2n1lrnL6pOm0Up7kwxhTW12Amm7WYkXR2qYrF2AmgmbAsxZjwy1xpg/m1Je2vrp8v/nz2xpmlBg4E9hrMU341wVpTOh/OfmGvAnra8q6uctr60ZQHV3Q+WMQJykMj8ZsWn2QBOmmHMB+m5pDIpTFonYigiaKAhGEiAHF7EliVnQkjoLVIMPtJpBKHYd3A8GYH9jJzrWwmHx5Qjp7vDAX0suGRym1vtm/9W1/HyR8vczfMs6Sk8DSv855/5dlX9oQq52hT8syyp2rx5Id17IAyAM3wIjQPMOHzytEB64q6D5zT91yNbnx3V/nqnd017S9Y0605k3izoXLpsxde2n38yoOV9s1LcjwzNjbdX6asnBVaBj/6/DwKwPkpcqbDG7BnsXoSqWnUAmottYF6jMSdVyYZh3zVXCjwTiwwHH6sGuRiEHQGzuRX6whZkp123oy1BWE2mEfJ/tvIRtM4ZM5bDXiMsPMaAKOTyc5uL57rqyyc5y5JE5pm1i2S2iUX0CcaQ6lC6Zog7JqSqZmYlosl2K6pwNA84zRnQW6SaALYZQGW5lhCtU/W34N6o+bKfZ8cf3/Cl/+iTX3wBzpOY4mRkeNf3rptycGSshQWgGbYt5jFc2e0+DglIrwl6DVWQ7BuwaJ3Xk1J4VL5urnLl/Wf+gHU/hZoZdKNym6lG+I34FaNeZKcSpJIo2IeCVvpdsDGfKvzJnAwmeD37Ow65ZWwSowpgwX5T69s/rB55dP5BcpgDKFV8p7q2sn/1uc93bVzT/w6UrCqDTWvfCq/oCD/qZXNoUj8BL5Kp6GU017frfNXkAtiiyf/SOCEeLqnd8R/Ql9GlCRfctS6k5chvIBuQ1zCCjoCHL2DHNHIXxMJ3kQeO8lbsUXONeSfA5EjcG6/E+KdhN4bP04vBhdi883+BFBzQbxFbvZzQeY9LNBZc0FNfn5NwfDn6rCTnTw6R8o+gfpf5hCom33cRuiTlss3KHmZjD+BPN+5gXuA2ziS/Q73mLxUkpbKN/eqwz5uK0X9F3h2d1V4nGNgZGBgAOJd776+iue3+crAzc4AAje5Bfcg0xz9YHEOBiYQBQA8FQlFAHicY2BkYGBnAAGOPgaG//85+hkYGVCBMgBGGwNYAAAAeJxjYGBgYB8EmKOPgQEAQ04BfgAAAAAAAA4AaAB+AMwA4AECAUIBbAGYAcICGAJYArQC4AMwA7AD3gQwBJYE3AUkBWYFigYgBmYGtAbqB1gIEghYCG4IhAi2COh4nGNgZGBgUGYoZWBnAAEmIOYCQgaG/2A+AwAYCQG2AHicXZBNaoNAGIZfE5PQCKFQ2lUps2oXBfOzzAESyDKBQJdGR2NQR3QSSE/QE/QEPUUPUHqsvsrXjTMw83zPvPMNCuAWP3DQDAejdm1GjzwS7pMmwi75XngAD4/CQ/oX4TFe4Qt7uMMbOzjuDc0EmXCP/C7cJ38Iu+RP4QEe8CU8pP8WHmOPX2EPz87TPo202ey2OjlnQSXV/6arOjWFmvszMWtd6CqwOlKHq6ovycLaWMWVydXKFFZnmVFlZU46tP7R2nI5ncbi/dDkfDtFBA2DDXbYkhKc+V0Bqs5Zt9JM1HQGBRTm/EezTmZNKtpcAMs9Yu6AK9caF76zoLWIWcfMGOSkVduvSWechqZsz040Ib2PY3urxBJTzriT95lipz+TN1fmAAAAeJxtkMl2wjAMRfOAhABlKm2h80C3+ajgCKKDY6cegP59TYBzukAL+z1Zsq8ctaJTTKPrsUQLbXQQI0EXKXroY4AbDDHCGBNMcYsZ7nCPB8yxwCOe8IwXvOIN7/jAJ76wxHfUqWX+OzgumWAjJMV17i0Ndlr6irLKO+qftdT7i6y4uFSUvCknay+lFYZIZaQcmfH/xIFdYn98bqhra1aKTM/6lWMnyaYirx1rFUQZFBkb2zJUtoXeJCeg0WnLtHeSFc3OtrnozNwqi0TkSpBMDB1nSde5oJXW23hTS2/T0LilglXX7dmFVxLnq5U0vYATHFk3zX3BOisoQHNDFDeZnqKDy9hRNawN7Vh727hFzcJ5c8TILrKZfH7tIPxAFP0BpLeJPA==) format("woff");font-weight:400;font-style:normal}.video-js .vjs-big-play-button .vjs-icon-placeholder:before,.video-js .vjs-play-control .vjs-icon-placeholder,.vjs-icon-play{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-big-play-button .vjs-icon-placeholder:before,.video-js .vjs-play-control .vjs-icon-placeholder:before,.vjs-icon-play:before{content:"\f101"}.vjs-icon-play-circle{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-play-circle:before{content:"\f102"}.video-js .vjs-play-control.vjs-playing .vjs-icon-placeholder,.vjs-icon-pause{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-play-control.vjs-playing .vjs-icon-placeholder:before,.vjs-icon-pause:before{content:"\f103"}.video-js .vjs-mute-control.vjs-vol-0 .vjs-icon-placeholder,.vjs-icon-volume-mute{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-mute-control.vjs-vol-0 .vjs-icon-placeholder:before,.vjs-icon-volume-mute:before{content:"\f104"}.video-js .vjs-mute-control.vjs-vol-1 .vjs-icon-placeholder,.vjs-icon-volume-low{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-mute-control.vjs-vol-1 .vjs-icon-placeholder:before,.vjs-icon-volume-low:before{content:"\f105"}.video-js .vjs-mute-control.vjs-vol-2 .vjs-icon-placeholder,.vjs-icon-volume-mid{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-mute-control.vjs-vol-2 .vjs-icon-placeholder:before,.vjs-icon-volume-mid:before{content:"\f106"}.video-js .vjs-mute-control .vjs-icon-placeholder,.vjs-icon-volume-high{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-mute-control .vjs-icon-placeholder:before,.vjs-icon-volume-high:before{content:"\f107"}.video-js .vjs-fullscreen-control .vjs-icon-placeholder,.vjs-icon-fullscreen-enter{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-fullscreen-control .vjs-icon-placeholder:before,.vjs-icon-fullscreen-enter:before{content:"\f108"}.video-js.vjs-fullscreen .vjs-fullscreen-control .vjs-icon-placeholder,.vjs-icon-fullscreen-exit{font-family:VideoJS;font-weight:400;font-style:normal}.video-js.vjs-fullscreen .vjs-fullscreen-control .vjs-icon-placeholder:before,.vjs-icon-fullscreen-exit:before{content:"\f109"}.vjs-icon-square{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-square:before{content:"\f10a"}.vjs-icon-spinner{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-spinner:before{content:"\f10b"}.video-js .vjs-subs-caps-button .vjs-icon-placeholder,.video-js .vjs-subtitles-button .vjs-icon-placeholder,.video-js.video-js:lang(en-AU) .vjs-subs-caps-button .vjs-icon-placeholder,.video-js.video-js:lang(en-GB) .vjs-subs-caps-button .vjs-icon-placeholder,.video-js.video-js:lang(en-IE) .vjs-subs-caps-button .vjs-icon-placeholder,.video-js.video-js:lang(en-NZ) .vjs-subs-caps-button .vjs-icon-placeholder,.vjs-icon-subtitles{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-subs-caps-button .vjs-icon-placeholder:before,.video-js .vjs-subtitles-button .vjs-icon-placeholder:before,.video-js.video-js:lang(en-AU) .vjs-subs-caps-button .vjs-icon-placeholder:before,.video-js.video-js:lang(en-GB) .vjs-subs-caps-button .vjs-icon-placeholder:before,.video-js.video-js:lang(en-IE) .vjs-subs-caps-button .vjs-icon-placeholder:before,.video-js.video-js:lang(en-NZ) .vjs-subs-caps-button .vjs-icon-placeholder:before,.vjs-icon-subtitles:before{content:"\f10c"}.video-js .vjs-captions-button .vjs-icon-placeholder,.video-js:lang(en) .vjs-subs-caps-button .vjs-icon-placeholder,.video-js:lang(fr-CA) .vjs-subs-caps-button .vjs-icon-placeholder,.vjs-icon-captions{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-captions-button .vjs-icon-placeholder:before,.video-js:lang(en) .vjs-subs-caps-button .vjs-icon-placeholder:before,.video-js:lang(fr-CA) .vjs-subs-caps-button .vjs-icon-placeholder:before,.vjs-icon-captions:before{content:"\f10d"}.video-js .vjs-chapters-button .vjs-icon-placeholder,.vjs-icon-chapters{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-chapters-button .vjs-icon-placeholder:before,.vjs-icon-chapters:before{content:"\f10e"}.vjs-icon-share{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-share:before{content:"\f10f"}.vjs-icon-cog{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-cog:before{content:"\f110"}.video-js .vjs-play-progress,.video-js .vjs-volume-level,.vjs-icon-circle,.vjs-seek-to-live-control .vjs-icon-placeholder{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-play-progress:before,.video-js .vjs-volume-level:before,.vjs-icon-circle:before,.vjs-seek-to-live-control .vjs-icon-placeholder:before{content:"\f111"}.vjs-icon-circle-outline{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-circle-outline:before{content:"\f112"}.vjs-icon-circle-inner-circle{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-circle-inner-circle:before{content:"\f113"}.vjs-icon-hd{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-hd:before{content:"\f114"}.video-js .vjs-control.vjs-close-button .vjs-icon-placeholder,.vjs-icon-cancel{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-control.vjs-close-button .vjs-icon-placeholder:before,.vjs-icon-cancel:before{content:"\f115"}.video-js .vjs-play-control.vjs-ended .vjs-icon-placeholder,.vjs-icon-replay{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-play-control.vjs-ended .vjs-icon-placeholder:before,.vjs-icon-replay:before{content:"\f116"}.vjs-icon-facebook{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-facebook:before{content:"\f117"}.vjs-icon-gplus{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-gplus:before{content:"\f118"}.vjs-icon-linkedin{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-linkedin:before{content:"\f119"}.vjs-icon-twitter{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-twitter:before{content:"\f11a"}.vjs-icon-tumblr{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-tumblr:before{content:"\f11b"}.vjs-icon-pinterest{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-pinterest:before{content:"\f11c"}.video-js .vjs-descriptions-button .vjs-icon-placeholder,.vjs-icon-audio-description{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-descriptions-button .vjs-icon-placeholder:before,.vjs-icon-audio-description:before{content:"\f11d"}.video-js .vjs-audio-button .vjs-icon-placeholder,.vjs-icon-audio{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-audio-button .vjs-icon-placeholder:before,.vjs-icon-audio:before{content:"\f11e"}.vjs-icon-next-item{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-next-item:before{content:"\f11f"}.vjs-icon-previous-item{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-previous-item:before{content:"\f120"}.video-js .vjs-picture-in-picture-control .vjs-icon-placeholder,.vjs-icon-picture-in-picture-enter{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-picture-in-picture-control .vjs-icon-placeholder:before,.vjs-icon-picture-in-picture-enter:before{content:"\f121"}.video-js.vjs-picture-in-picture .vjs-picture-in-picture-control .vjs-icon-placeholder,.vjs-icon-picture-in-picture-exit{font-family:VideoJS;font-weight:400;font-style:normal}.video-js.vjs-picture-in-picture .vjs-picture-in-picture-control .vjs-icon-placeholder:before,.vjs-icon-picture-in-picture-exit:before{content:"\f122"}.video-js{display:block;vertical-align:top;box-sizing:border-box;color:#fff;background-color:#000;position:relative;padding:0;font-size:10px;line-height:1;font-weight:400;font-style:normal;font-family:Arial,Helvetica,sans-serif;word-break:initial}.video-js:-moz-full-screen{position:absolute}.video-js:-webkit-full-screen{width:100%!important;height:100%!important}.video-js[tabindex="-1"]{outline:0}.video-js *,.video-js :after,.video-js :before{box-sizing:inherit}.video-js ul{font-family:inherit;font-size:inherit;line-height:inherit;list-style-position:outside;margin-left:0;margin-right:0;margin-top:0;margin-bottom:0}.video-js.vjs-16-9,.video-js.vjs-4-3,.video-js.vjs-fluid{width:100%;max-width:100%;height:0}.video-js.vjs-16-9{padding-top:56.25%}.video-js.vjs-4-3{padding-top:75%}.video-js.vjs-fill{width:100%;height:100%}.video-js .vjs-tech{position:absolute;top:0;left:0;width:100%;height:100%}body.vjs-full-window{padding:0;margin:0;height:100%}.vjs-full-window .video-js.vjs-fullscreen{position:fixed;overflow:hidden;z-index:1000;left:0;top:0;bottom:0;right:0}.video-js.vjs-fullscreen{width:100%!important;height:100%!important;padding-top:0!important}.video-js.vjs-fullscreen.vjs-user-inactive{cursor:none}.vjs-hidden{display:none!important}.vjs-disabled{opacity:.5;cursor:default}.video-js .vjs-offscreen{height:1px;left:-9999px;position:absolute;top:0;width:1px}.vjs-lock-showing{display:block!important;opacity:1;visibility:visible}.vjs-no-js{padding:20px;color:#fff;background-color:#000;font-size:18px;font-family:Arial,Helvetica,sans-serif;text-align:center;width:300px;height:150px;margin:0 auto}.vjs-no-js a,.vjs-no-js a:visited{color:#66a8cc}.video-js .vjs-big-play-button{font-size:3em;line-height:1.5em;height:1.63332em;width:3em;display:block;position:absolute;top:10px;left:10px;padding:0;cursor:pointer;opacity:1;border:.06666em solid #fff;background-color:#2b333f;background-color:rgba(43,51,63,.7);border-radius:.3em;transition:all .4s}.vjs-big-play-centered .vjs-big-play-button{top:50%;left:50%;margin-top:-.81666em;margin-left:-1.5em}.video-js .vjs-big-play-button:focus,.video-js:hover .vjs-big-play-button{border-color:#fff;background-color:#73859f;background-color:rgba(115,133,159,.5);transition:all 0s}.vjs-controls-disabled .vjs-big-play-button,.vjs-error .vjs-big-play-button,.vjs-has-started .vjs-big-play-button,.vjs-using-native-controls .vjs-big-play-button{display:none}.vjs-has-started.vjs-paused.vjs-show-big-play-button-on-pause .vjs-big-play-button{display:block}.video-js button{background:0 0;border:none;color:inherit;display:inline-block;font-size:inherit;line-height:inherit;text-transform:none;text-decoration:none;transition:none;-webkit-appearance:none;-moz-appearance:none;appearance:none}.vjs-control .vjs-button{width:100%;height:100%}.video-js .vjs-control.vjs-close-button{cursor:pointer;height:3em;position:absolute;right:0;top:.5em;z-index:2}.video-js .vjs-modal-dialog{background:rgba(0,0,0,.8);background:linear-gradient(180deg,rgba(0,0,0,.8),rgba(255,255,255,0));overflow:auto}.video-js .vjs-modal-dialog>*{box-sizing:border-box}.vjs-modal-dialog .vjs-modal-dialog-content{font-size:1.2em;line-height:1.5;padding:20px 24px;z-index:1}.vjs-menu-button{cursor:pointer}.vjs-menu-button.vjs-disabled{cursor:default}.vjs-workinghover .vjs-menu-button.vjs-disabled:hover .vjs-menu{display:none}.vjs-menu .vjs-menu-content{display:block;padding:0;margin:0;font-family:Arial,Helvetica,sans-serif;overflow:auto}.vjs-menu .vjs-menu-content>*{box-sizing:border-box}.vjs-scrubbing .vjs-control.vjs-menu-button:hover .vjs-menu{display:none}.vjs-menu li{list-style:none;margin:0;padding:.2em 0;line-height:1.4em;font-size:1.2em;text-align:center;text-transform:lowercase}.js-focus-visible .vjs-menu li.vjs-menu-item:hover,.vjs-menu li.vjs-menu-item:focus,.vjs-menu li.vjs-menu-item:hover{background-color:#73859f;background-color:rgba(115,133,159,.5)}.js-focus-visible .vjs-menu li.vjs-selected:hover,.vjs-menu li.vjs-selected,.vjs-menu li.vjs-selected:focus,.vjs-menu li.vjs-selected:hover{background-color:#fff;color:#2b333f}.vjs-menu li.vjs-menu-title{text-align:center;text-transform:uppercase;font-size:1em;line-height:2em;padding:0;margin:0 0 .3em 0;font-weight:700;cursor:default}.vjs-menu-button-popup .vjs-menu{display:none;position:absolute;bottom:0;width:10em;left:-3em;height:0;margin-bottom:1.5em;border-top-color:rgba(43,51,63,.7)}.vjs-menu-button-popup .vjs-menu .vjs-menu-content{background-color:#2b333f;background-color:rgba(43,51,63,.7);position:absolute;width:100%;bottom:1.5em;max-height:15em}.vjs-layout-tiny .vjs-menu-button-popup .vjs-menu .vjs-menu-content,.vjs-layout-x-small .vjs-menu-button-popup .vjs-menu .vjs-menu-content{max-height:5em}.vjs-layout-small .vjs-menu-button-popup .vjs-menu .vjs-menu-content{max-height:10em}.vjs-layout-medium .vjs-menu-button-popup .vjs-menu .vjs-menu-content{max-height:14em}.vjs-layout-huge .vjs-menu-button-popup .vjs-menu .vjs-menu-content,.vjs-layout-large .vjs-menu-button-popup .vjs-menu .vjs-menu-content,.vjs-layout-x-large .vjs-menu-button-popup .vjs-menu .vjs-menu-content{max-height:25em}.vjs-menu-button-popup .vjs-menu.vjs-lock-showing,.vjs-workinghover .vjs-menu-button-popup.vjs-hover .vjs-menu{display:block}.video-js .vjs-menu-button-inline{transition:all .4s;overflow:hidden}.video-js .vjs-menu-button-inline:before{width:2.222222222em}.video-js .vjs-menu-button-inline.vjs-slider-active,.video-js .vjs-menu-button-inline:focus,.video-js .vjs-menu-button-inline:hover,.video-js.vjs-no-flex .vjs-menu-button-inline{width:12em}.vjs-menu-button-inline .vjs-menu{opacity:0;height:100%;width:auto;position:absolute;left:4em;top:0;padding:0;margin:0;transition:all .4s}.vjs-menu-button-inline.vjs-slider-active .vjs-menu,.vjs-menu-button-inline:focus .vjs-menu,.vjs-menu-button-inline:hover .vjs-menu{display:block;opacity:1}.vjs-no-flex .vjs-menu-button-inline .vjs-menu{display:block;opacity:1;position:relative;width:auto}.vjs-no-flex .vjs-menu-button-inline.vjs-slider-active .vjs-menu,.vjs-no-flex .vjs-menu-button-inline:focus .vjs-menu,.vjs-no-flex .vjs-menu-button-inline:hover .vjs-menu{width:auto}.vjs-menu-button-inline .vjs-menu-content{width:auto;height:100%;margin:0;overflow:hidden}.video-js .vjs-control-bar{display:none;width:100%;position:absolute;bottom:0;left:0;right:0;height:3em;background-color:#2b333f;background-color:rgba(43,51,63,.7)}.vjs-has-started .vjs-control-bar{display:flex;visibility:visible;opacity:1;transition:visibility .1s,opacity .1s}.vjs-has-started.vjs-user-inactive.vjs-playing .vjs-control-bar{visibility:visible;opacity:0;transition:visibility 1s,opacity 1s}.vjs-controls-disabled .vjs-control-bar,.vjs-error .vjs-control-bar,.vjs-using-native-controls .vjs-control-bar{display:none!important}.vjs-audio.vjs-has-started.vjs-user-inactive.vjs-playing .vjs-control-bar{opacity:1;visibility:visible}.vjs-has-started.vjs-no-flex .vjs-control-bar{display:table}.video-js .vjs-control{position:relative;text-align:center;margin:0;padding:0;height:100%;width:4em;flex:none}.vjs-button>.vjs-icon-placeholder:before{font-size:1.8em;line-height:1.67}.video-js .vjs-control:focus,.video-js .vjs-control:focus:before,.video-js .vjs-control:hover:before{text-shadow:0 0 1em #fff}.video-js .vjs-control-text{border:0;clip:rect(0 0 0 0);height:1px;overflow:hidden;padding:0;position:absolute;width:1px}.vjs-no-flex .vjs-control{display:table-cell;vertical-align:middle}.video-js .vjs-custom-control-spacer{display:none}.video-js .vjs-progress-control{cursor:pointer;flex:auto;display:flex;align-items:center;min-width:4em;touch-action:none}.video-js .vjs-progress-control.disabled{cursor:default}.vjs-live .vjs-progress-control{display:none}.vjs-liveui .vjs-progress-control{display:flex;align-items:center}.vjs-no-flex .vjs-progress-control{width:auto}.video-js .vjs-progress-holder{flex:auto;transition:all .2s;height:.3em}.video-js .vjs-progress-control .vjs-progress-holder{margin:0 10px}.video-js .vjs-progress-control:hover .vjs-progress-holder{font-size:1.6666666667em}.video-js .vjs-progress-control:hover .vjs-progress-holder.disabled{font-size:1em}.video-js .vjs-progress-holder .vjs-load-progress,.video-js .vjs-progress-holder .vjs-load-progress div,.video-js .vjs-progress-holder .vjs-play-progress{position:absolute;display:block;height:100%;margin:0;padding:0;width:0}.video-js .vjs-play-progress{background-color:#fff}.video-js .vjs-play-progress:before{font-size:.9em;position:absolute;right:-.5em;top:-.3333333333em;z-index:1}.video-js .vjs-load-progress{background:rgba(115,133,159,.5)}.video-js .vjs-load-progress div{background:rgba(115,133,159,.75)}.video-js .vjs-time-tooltip{background-color:#fff;background-color:rgba(255,255,255,.8);border-radius:.3em;color:#000;float:right;font-family:Arial,Helvetica,sans-serif;font-size:1em;padding:6px 8px 8px 8px;pointer-events:none;position:absolute;top:-3.4em;visibility:hidden;z-index:1}.video-js .vjs-progress-holder:focus .vjs-time-tooltip{display:none}.video-js .vjs-progress-control:hover .vjs-progress-holder:focus .vjs-time-tooltip,.video-js .vjs-progress-control:hover .vjs-time-tooltip{display:block;font-size:.6em;visibility:visible}.video-js .vjs-progress-control.disabled:hover .vjs-time-tooltip{font-size:1em}.video-js .vjs-progress-control .vjs-mouse-display{display:none;position:absolute;width:1px;height:100%;background-color:#000;z-index:1}.vjs-no-flex .vjs-progress-control .vjs-mouse-display{z-index:0}.video-js .vjs-progress-control:hover .vjs-mouse-display{display:block}.video-js.vjs-user-inactive .vjs-progress-control .vjs-mouse-display{visibility:hidden;opacity:0;transition:visibility 1s,opacity 1s}.video-js.vjs-user-inactive.vjs-no-flex .vjs-progress-control .vjs-mouse-display{display:none}.vjs-mouse-display .vjs-time-tooltip{color:#fff;background-color:#000;background-color:rgba(0,0,0,.8)}.video-js .vjs-slider{position:relative;cursor:pointer;padding:0;margin:0 .45em 0 .45em;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:#73859f;background-color:rgba(115,133,159,.5)}.video-js .vjs-slider.disabled{cursor:default}.video-js .vjs-slider:focus{text-shadow:0 0 1em #fff;box-shadow:0 0 1em #fff}.video-js .vjs-mute-control{cursor:pointer;flex:none}.video-js .vjs-volume-control{cursor:pointer;margin-right:1em;display:flex}.video-js .vjs-volume-control.vjs-volume-horizontal{width:5em}.video-js .vjs-volume-panel .vjs-volume-control{visibility:visible;opacity:0;width:1px;height:1px;margin-left:-1px}.video-js .vjs-volume-panel{transition:width 1s}.video-js .vjs-volume-panel .vjs-volume-control.vjs-slider-active,.video-js .vjs-volume-panel .vjs-volume-control:active,.video-js .vjs-volume-panel.vjs-hover .vjs-mute-control~.vjs-volume-control,.video-js .vjs-volume-panel.vjs-hover .vjs-volume-control,.video-js .vjs-volume-panel:active .vjs-volume-control,.video-js .vjs-volume-panel:focus .vjs-volume-control{visibility:visible;opacity:1;position:relative;transition:visibility .1s,opacity .1s,height .1s,width .1s,left 0s,top 0s}.video-js .vjs-volume-panel .vjs-volume-control.vjs-slider-active.vjs-volume-horizontal,.video-js .vjs-volume-panel .vjs-volume-control:active.vjs-volume-horizontal,.video-js .vjs-volume-panel.vjs-hover .vjs-mute-control~.vjs-volume-control.vjs-volume-horizontal,.video-js .vjs-volume-panel.vjs-hover .vjs-volume-control.vjs-volume-horizontal,.video-js .vjs-volume-panel:active .vjs-volume-control.vjs-volume-horizontal,.video-js .vjs-volume-panel:focus .vjs-volume-control.vjs-volume-horizontal{width:5em;height:3em;margin-right:0}.video-js .vjs-volume-panel .vjs-volume-control.vjs-slider-active.vjs-volume-vertical,.video-js .vjs-volume-panel .vjs-volume-control:active.vjs-volume-vertical,.video-js .vjs-volume-panel.vjs-hover .vjs-mute-control~.vjs-volume-control.vjs-volume-vertical,.video-js .vjs-volume-panel.vjs-hover .vjs-volume-control.vjs-volume-vertical,.video-js .vjs-volume-panel:active .vjs-volume-control.vjs-volume-vertical,.video-js .vjs-volume-panel:focus .vjs-volume-control.vjs-volume-vertical{left:-3.5em;transition:left 0s}.video-js .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-hover,.video-js .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-slider-active,.video-js .vjs-volume-panel.vjs-volume-panel-horizontal:active{width:10em;transition:width .1s}.video-js .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-mute-toggle-only{width:4em}.video-js .vjs-volume-panel .vjs-volume-control.vjs-volume-vertical{height:8em;width:3em;left:-3000em;transition:visibility 1s,opacity 1s,height 1s 1s,width 1s 1s,left 1s 1s,top 1s 1s}.video-js .vjs-volume-panel .vjs-volume-control.vjs-volume-horizontal{transition:visibility 1s,opacity 1s,height 1s 1s,width 1s,left 1s 1s,top 1s 1s}.video-js.vjs-no-flex .vjs-volume-panel .vjs-volume-control.vjs-volume-horizontal{width:5em;height:3em;visibility:visible;opacity:1;position:relative;transition:none}.video-js.vjs-no-flex .vjs-volume-control.vjs-volume-vertical,.video-js.vjs-no-flex .vjs-volume-panel .vjs-volume-control.vjs-volume-vertical{position:absolute;bottom:3em;left:.5em}.video-js .vjs-volume-panel{display:flex}.video-js .vjs-volume-bar{margin:1.35em .45em}.vjs-volume-bar.vjs-slider-horizontal{width:5em;height:.3em}.vjs-volume-bar.vjs-slider-vertical{width:.3em;height:5em;margin:1.35em auto}.video-js .vjs-volume-level{position:absolute;bottom:0;left:0;background-color:#fff}.video-js .vjs-volume-level:before{position:absolute;font-size:.9em}.vjs-slider-vertical .vjs-volume-level{width:.3em}.vjs-slider-vertical .vjs-volume-level:before{top:-.5em;left:-.3em}.vjs-slider-horizontal .vjs-volume-level{height:.3em}.vjs-slider-horizontal .vjs-volume-level:before{top:-.3em;right:-.5em}.video-js .vjs-volume-panel.vjs-volume-panel-vertical{width:4em}.vjs-volume-bar.vjs-slider-vertical .vjs-volume-level{height:100%}.vjs-volume-bar.vjs-slider-horizontal .vjs-volume-level{width:100%}.video-js .vjs-volume-vertical{width:3em;height:8em;bottom:8em;background-color:#2b333f;background-color:rgba(43,51,63,.7)}.video-js .vjs-volume-horizontal .vjs-menu{left:-2em}.vjs-poster{display:inline-block;vertical-align:middle;background-repeat:no-repeat;background-position:50% 50%;background-size:contain;background-color:#000;cursor:pointer;margin:0;padding:0;position:absolute;top:0;right:0;bottom:0;left:0;height:100%}.vjs-has-started .vjs-poster{display:none}.vjs-audio.vjs-has-started .vjs-poster{display:block}.vjs-using-native-controls .vjs-poster{display:none}.video-js .vjs-live-control{display:flex;align-items:flex-start;flex:auto;font-size:1em;line-height:3em}.vjs-no-flex .vjs-live-control{display:table-cell;width:auto;text-align:left}.video-js.vjs-liveui .vjs-live-control,.video-js:not(.vjs-live) .vjs-live-control{display:none}.video-js .vjs-seek-to-live-control{cursor:pointer;flex:none;display:inline-flex;height:100%;padding-left:.5em;padding-right:.5em;font-size:1em;line-height:3em;width:auto;min-width:4em}.vjs-no-flex .vjs-seek-to-live-control{display:table-cell;width:auto;text-align:left}.video-js.vjs-live:not(.vjs-liveui) .vjs-seek-to-live-control,.video-js:not(.vjs-live) .vjs-seek-to-live-control{display:none}.vjs-seek-to-live-control.vjs-control.vjs-at-live-edge{cursor:auto}.vjs-seek-to-live-control .vjs-icon-placeholder{margin-right:.5em;color:#888}.vjs-seek-to-live-control.vjs-control.vjs-at-live-edge .vjs-icon-placeholder{color:red}.video-js .vjs-time-control{flex:none;font-size:1em;line-height:3em;min-width:2em;width:auto;padding-left:1em;padding-right:1em}.vjs-live .vjs-time-control{display:none}.video-js .vjs-current-time,.vjs-no-flex .vjs-current-time{display:none}.video-js .vjs-duration,.vjs-no-flex .vjs-duration{display:none}.vjs-time-divider{display:none;line-height:3em}.vjs-live .vjs-time-divider{display:none}.video-js .vjs-play-control{cursor:pointer}.video-js .vjs-play-control .vjs-icon-placeholder{flex:none}.vjs-text-track-display{position:absolute;bottom:3em;left:0;right:0;top:0;pointer-events:none}.video-js.vjs-user-inactive.vjs-playing .vjs-text-track-display{bottom:1em}.video-js .vjs-text-track{font-size:1.4em;text-align:center;margin-bottom:.1em}.vjs-subtitles{color:#fff}.vjs-captions{color:#fc6}.vjs-tt-cue{display:block}video::-webkit-media-text-track-display{transform:translateY(-3em)}.video-js.vjs-user-inactive.vjs-playing video::-webkit-media-text-track-display{transform:translateY(-1.5em)}.video-js .vjs-picture-in-picture-control{cursor:pointer;flex:none}.video-js .vjs-fullscreen-control{cursor:pointer;flex:none}.vjs-playback-rate .vjs-playback-rate-value,.vjs-playback-rate>.vjs-menu-button{position:absolute;top:0;left:0;width:100%;height:100%}.vjs-playback-rate .vjs-playback-rate-value{pointer-events:none;font-size:1.5em;line-height:2;text-align:center}.vjs-playback-rate .vjs-menu{width:4em;left:0}.vjs-error .vjs-error-display .vjs-modal-dialog-content{font-size:1.4em;text-align:center}.vjs-error .vjs-error-display:before{color:#fff;content:"X";font-family:Arial,Helvetica,sans-serif;font-size:4em;left:0;line-height:1;margin-top:-.5em;position:absolute;text-shadow:.05em .05em .1em #000;text-align:center;top:50%;vertical-align:middle;width:100%}.vjs-loading-spinner{display:none;position:absolute;top:50%;left:50%;margin:-25px 0 0 -25px;opacity:.85;text-align:left;border:6px solid rgba(43,51,63,.7);box-sizing:border-box;background-clip:padding-box;width:50px;height:50px;border-radius:25px;visibility:hidden}.vjs-seeking .vjs-loading-spinner,.vjs-waiting .vjs-loading-spinner{display:block;-webkit-animation:vjs-spinner-show 0s linear .3s forwards;animation:vjs-spinner-show 0s linear .3s forwards}.vjs-loading-spinner:after,.vjs-loading-spinner:before{content:"";position:absolute;margin:-6px;box-sizing:inherit;width:inherit;height:inherit;border-radius:inherit;opacity:1;border:inherit;border-color:transparent;border-top-color:#fff}.vjs-seeking .vjs-loading-spinner:after,.vjs-seeking .vjs-loading-spinner:before,.vjs-waiting .vjs-loading-spinner:after,.vjs-waiting .vjs-loading-spinner:before{-webkit-animation:vjs-spinner-spin 1.1s cubic-bezier(.6,.2,0,.8) infinite,vjs-spinner-fade 1.1s linear infinite;animation:vjs-spinner-spin 1.1s cubic-bezier(.6,.2,0,.8) infinite,vjs-spinner-fade 1.1s linear infinite}.vjs-seeking .vjs-loading-spinner:before,.vjs-waiting .vjs-loading-spinner:before{border-top-color:#fff}.vjs-seeking .vjs-loading-spinner:after,.vjs-waiting .vjs-loading-spinner:after{border-top-color:#fff;-webkit-animation-delay:.44s;animation-delay:.44s}@keyframes vjs-spinner-show{to{visibility:visible}}@-webkit-keyframes vjs-spinner-show{to{visibility:visible}}@keyframes vjs-spinner-spin{100%{transform:rotate(360deg)}}@-webkit-keyframes vjs-spinner-spin{100%{-webkit-transform:rotate(360deg)}}@keyframes vjs-spinner-fade{0%{border-top-color:#73859f}20%{border-top-color:#73859f}35%{border-top-color:#fff}60%{border-top-color:#73859f}100%{border-top-color:#73859f}}@-webkit-keyframes vjs-spinner-fade{0%{border-top-color:#73859f}20%{border-top-color:#73859f}35%{border-top-color:#fff}60%{border-top-color:#73859f}100%{border-top-color:#73859f}}.vjs-chapters-button .vjs-menu ul{width:24em}.video-js .vjs-subs-caps-button+.vjs-menu .vjs-captions-menu-item .vjs-menu-item-text .vjs-icon-placeholder{vertical-align:middle;display:inline-block;margin-bottom:-.1em}.video-js .vjs-subs-caps-button+.vjs-menu .vjs-captions-menu-item .vjs-menu-item-text .vjs-icon-placeholder:before{font-family:VideoJS;content:"";font-size:1.5em;line-height:inherit}.video-js .vjs-audio-button+.vjs-menu .vjs-main-desc-menu-item .vjs-menu-item-text .vjs-icon-placeholder{vertical-align:middle;display:inline-block;margin-bottom:-.1em}.video-js .vjs-audio-button+.vjs-menu .vjs-main-desc-menu-item .vjs-menu-item-text .vjs-icon-placeholder:before{font-family:VideoJS;content:" ";font-size:1.5em;line-height:inherit}.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-audio-button,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-captions-button,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-chapters-button,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-current-time,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-descriptions-button,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-duration,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-playback-rate,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-remaining-time,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-subtitles-button,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-time-divider,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-volume-control,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-audio-button,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-captions-button,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-chapters-button,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-current-time,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-descriptions-button,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-duration,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-playback-rate,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-remaining-time,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-subtitles-button,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-time-divider,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-volume-control,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-audio-button,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-captions-button,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-chapters-button,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-current-time,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-descriptions-button,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-duration,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-playback-rate,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-remaining-time,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-subtitles-button,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-time-divider,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-volume-control{display:none}.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-slider-active,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-volume-panel.vjs-volume-panel-horizontal:active,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-volume-panel.vjs-volume-panel-horizontal:hover,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-slider-active,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-volume-panel.vjs-volume-panel-horizontal:active,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-volume-panel.vjs-volume-panel-horizontal:hover,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-slider-active,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-volume-panel.vjs-volume-panel-horizontal:active,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-volume-panel.vjs-volume-panel-horizontal:hover{width:auto;width:initial}.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-subs-caps-button,.video-js:not(.vjs-fullscreen).vjs-layout-x-small:not(.vjs-live) .vjs-subs-caps-button,.video-js:not(.vjs-fullscreen).vjs-layout-x-small:not(.vjs-liveui) .vjs-subs-caps-button{display:none}.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-custom-control-spacer,.video-js:not(.vjs-fullscreen).vjs-layout-x-small.vjs-liveui .vjs-custom-control-spacer{flex:auto;display:block}.video-js:not(.vjs-fullscreen).vjs-layout-tiny.vjs-no-flex .vjs-custom-control-spacer,.video-js:not(.vjs-fullscreen).vjs-layout-x-small.vjs-liveui.vjs-no-flex .vjs-custom-control-spacer{width:auto}.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-progress-control,.video-js:not(.vjs-fullscreen).vjs-layout-x-small.vjs-liveui .vjs-progress-control{display:none}.vjs-modal-dialog.vjs-text-track-settings{background-color:#2b333f;background-color:rgba(43,51,63,.75);color:#fff;height:70%}.vjs-text-track-settings .vjs-modal-dialog-content{display:table}.vjs-text-track-settings .vjs-track-settings-colors,.vjs-text-track-settings .vjs-track-settings-controls,.vjs-text-track-settings .vjs-track-settings-font{display:table-cell}.vjs-text-track-settings .vjs-track-settings-controls{text-align:right;vertical-align:bottom}@supports (display:grid){.vjs-text-track-settings .vjs-modal-dialog-content{display:grid;grid-template-columns:1fr 1fr;grid-template-rows:1fr;padding:20px 24px 0 24px}.vjs-track-settings-controls .vjs-default-button{margin-bottom:20px}.vjs-text-track-settings .vjs-track-settings-controls{grid-column:1/-1}.vjs-layout-small .vjs-text-track-settings .vjs-modal-dialog-content,.vjs-layout-tiny .vjs-text-track-settings .vjs-modal-dialog-content,.vjs-layout-x-small .vjs-text-track-settings .vjs-modal-dialog-content{grid-template-columns:1fr}}.vjs-track-setting>select{margin-right:1em;margin-bottom:.5em}.vjs-text-track-settings fieldset{margin:5px;padding:3px;border:none}.vjs-text-track-settings fieldset span{display:inline-block}.vjs-text-track-settings fieldset span>select{max-width:7.3em}.vjs-text-track-settings legend{color:#fff;margin:0 0 5px 0}.vjs-text-track-settings .vjs-label{position:absolute;clip:rect(1px 1px 1px 1px);clip:rect(1px,1px,1px,1px);display:block;margin:0 0 5px 0;padding:0;border:0;height:1px;width:1px;overflow:hidden}.vjs-track-settings-controls button:active,.vjs-track-settings-controls button:focus{outline-style:solid;outline-width:medium;background-image:linear-gradient(0deg,#fff 88%,#73859f 100%)}.vjs-track-settings-controls button:hover{color:rgba(43,51,63,.75)}.vjs-track-settings-controls button{background-color:#fff;background-image:linear-gradient(-180deg,#fff 88%,#73859f 100%);color:#2b333f;cursor:pointer;border-radius:2px}.vjs-track-settings-controls .vjs-default-button{margin-right:1em}@media print{.video-js>:not(.vjs-tech):not(.vjs-poster){visibility:hidden}}.vjs-resize-manager{position:absolute;top:0;left:0;width:100%;height:100%;border:none;z-index:-1000}.js-focus-visible .video-js :focus:not(.focus-visible){outline:0;background:0 0}.video-js .vjs-menu :focus:not(:focus-visible),.video-js :focus:not(:focus-visible){outline:0;background:0 0} \ No newline at end of file diff --git a/assets/js/video.min.js b/assets/js/video.min.js index 10743dd4..ed10c74e 100644 --- a/assets/js/video.min.js +++ b/assets/js/video.min.js @@ -1,6 +1,6 @@ /** * @license - * Video.js 7.6.0 + * Video.js 7.6.6 * Copyright Brightcove, Inc. * Available under Apache License Version 2.0 * @@ -9,12 +9,13 @@ * Available under Apache License Version 2.0 * */ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("global/window"),require("global/document")):"function"==typeof define&&define.amd?define(["global/window","global/document"],t):(e=e||self).videojs=t(e.window,e.document)}(this,function(v,h){v=v&&v.hasOwnProperty("default")?v.default:v,h=h&&h.hasOwnProperty("default")?h.default:h;var d="7.6.0";function p(e,t){e.prototype=Object.create(t.prototype),(e.prototype.constructor=e).__proto__=t}function a(e,t){return(a=Object.setPrototypeOf||function(e,t){return e.__proto__=t,e})(e,t)}function s(e,t,i){return(s=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],function(){})),!0}catch(e){return!1}}()?Reflect.construct:function(e,t,i){var n=[null];n.push.apply(n,t);var r=new(Function.bind.apply(e,n));return i&&a(r,i.prototype),r}).apply(null,arguments)}function f(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}function t(e,t){return t=t||e.slice(0),e.raw=t,e}var u=[],e=function(s,o){return function(e,t,i){var n=o.levels[t],r=new RegExp("^("+n+")$");if("log"!==e&&i.unshift(e.toUpperCase()+":"),i.unshift(s+":"),u&&u.push([].concat(i)),v.console){var a=v.console[e];a||"debug"!==e||(a=v.console.info||v.console.log),a&&n&&r.test(e)&&a[Array.isArray(i)?"apply":"call"](v.console,i)}}};var m=function t(i){function n(){for(var e=arguments.length,t=new Array(e),i=0;i',i=n.firstChild,n.setAttribute("style","display:none; position:absolute;"),h.body.appendChild(n));for(var a={},s=0;sx',e=t.firstChild.href}return e}function Ft(e){if("string"==typeof e){var t=/^(\/?)([\s\S]*?)((?:\.{1,2}|[^\/]+?)(\.([^\.\/\?]+)))(?:[\/]*|[\?].*)$/i.exec(e);if(t)return t.pop().toLowerCase()}return""}function Vt(e){var t=v.location,i=Nt(e);return(":"===i.protocol?t.protocol:i.protocol)+i.host!==t.protocol+t.host}var Ht=function(n){function e(e){var t;void 0===e&&(e=[]);for(var i=e.length-1;0<=i;i--)if(e[i].enabled){Mt(e,e[i]);break}return(t=n.call(this,e)||this).changing_=!1,t}p(e,n);var t=e.prototype;return t.addTrack=function(e){var t=this;e.enabled&&Mt(this,e),n.prototype.addTrack.call(this,e),e.addEventListener&&(e.enabledChange_=function(){t.changing_||(t.changing_=!0,Mt(t,e),t.changing_=!1,t.trigger("change"))},e.addEventListener("enabledchange",e.enabledChange_))},t.removeTrack=function(e){n.prototype.removeTrack.call(this,e),e.removeEventListener&&e.enabledChange_&&(e.removeEventListener("enabledchange",e.enabledChange_),e.enabledChange_=null)},e}(Dt),qt=function(n){function e(e){var t;void 0===e&&(e=[]);for(var i=e.length-1;0<=i;i--)if(e[i].selected){Bt(e,e[i]);break}return(t=n.call(this,e)||this).changing_=!1,Object.defineProperty(f(t),"selectedIndex",{get:function(){for(var e=0;e>0},ToUint32:function(e){return this.ToNumber(e)>>>0},ToUint16:function(e){var t=this.ToNumber(e);return Ki(t)||0===t||!Qi(t)?0:function(e,t){var i=e%t;return Math.floor(0<=i?i:i+t)}(xi(t)*Math.floor(Math.abs(t)),65536)},ToString:function(e){return on(e)},ToObject:function(e){return this.CheckObjectCoercible(e),an(e)},CheckObjectCoercible:function(e,t){if(null==e)throw new sn(t||"Cannot call method on "+e);return e},IsCallable:Di,SameValue:function(e,t){return e===t?0!==e||1/e==1/t:Ki(e)&&Ki(t)},Type:function(e){return null===e?"Null":"undefined"==typeof e?"Undefined":"function"==typeof e||"object"==typeof e?"Object":"number"==typeof e?"Number":"boolean"==typeof e?"Boolean":"string"==typeof e?"String":void 0},IsPropertyDescriptor:function(e){if("Object"!==this.Type(e))return!1;var t={"[[Configurable]]":!0,"[[Enumerable]]":!0,"[[Get]]":!0,"[[Set]]":!0,"[[Value]]":!0,"[[Writable]]":!0};for(var i in e)if(Wi(e,i)&&!t[i])return!1;var n=Wi(e,"[[Value]]"),r=Wi(e,"[[Get]]")||Wi(e,"[[Set]]");if(n&&r)throw new sn("Property Descriptors may not be both accessor and data descriptors");return!0},IsAccessorDescriptor:function(e){return"undefined"!=typeof e&&(Oi(this,"Property Descriptor","Desc",e),!(!Wi(e,"[[Get]]")&&!Wi(e,"[[Set]]")))},IsDataDescriptor:function(e){return"undefined"!=typeof e&&(Oi(this,"Property Descriptor","Desc",e),!(!Wi(e,"[[Value]]")&&!Wi(e,"[[Writable]]")))},IsGenericDescriptor:function(e){return"undefined"!=typeof e&&(Oi(this,"Property Descriptor","Desc",e),!this.IsAccessorDescriptor(e)&&!this.IsDataDescriptor(e))},FromPropertyDescriptor:function(e){if("undefined"==typeof e)return e;if(Oi(this,"Property Descriptor","Desc",e),this.IsDataDescriptor(e))return{value:e["[[Value]]"],writable:!!e["[[Writable]]"],enumerable:!!e["[[Enumerable]]"],configurable:!!e["[[Configurable]]"]};if(this.IsAccessorDescriptor(e))return{get:e["[[Get]]"],set:e["[[Set]]"],enumerable:!!e["[[Enumerable]]"],configurable:!!e["[[Configurable]]"]};throw new sn("FromPropertyDescriptor must be called with a fully populated Property Descriptor")},ToPropertyDescriptor:function(e){if("Object"!==this.Type(e))throw new sn("ToPropertyDescriptor requires an object");var t={};if(Wi(e,"enumerable")&&(t["[[Enumerable]]"]=this.ToBoolean(e.enumerable)),Wi(e,"configurable")&&(t["[[Configurable]]"]=this.ToBoolean(e.configurable)),Wi(e,"value")&&(t["[[Value]]"]=e.value),Wi(e,"writable")&&(t["[[Writable]]"]=this.ToBoolean(e.writable)),Wi(e,"get")){var i=e.get;if("undefined"!=typeof i&&!this.IsCallable(i))throw new TypeError("getter must be a function");t["[[Get]]"]=i}if(Wi(e,"set")){var n=e.set;if("undefined"!=typeof n&&!this.IsCallable(n))throw new sn("setter must be a function");t["[[Set]]"]=n}if((Wi(t,"[[Get]]")||Wi(t,"[[Set]]"))&&(Wi(t,"[[Value]]")||Wi(t,"[[Writable]]")))throw new sn("Invalid property descriptor. Cannot both specify accessors and a value or writable attribute");return t}},ln=ai.call(Function.call,String.prototype.replace),cn=/^[\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028\u2029\uFEFF]+/,hn=/[\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028\u2029\uFEFF]+$/,dn=ai.call(Function.call,Bi());ji(dn,{getPolyfill:Bi,implementation:Mi,shim:function(){var e=Bi();return ji(String.prototype,{trim:e},{trim:function(){return String.prototype.trim!==e}}),e}});var pn=dn,fn=Object.prototype.toString,mn=Object.prototype.hasOwnProperty,gn=function(e,t,i){if(!Di(t))throw new TypeError("iterator must be a function");var n;3<=arguments.length&&(n=i),"[object Array]"===fn.call(e)?function(e,t,i){for(var n=0,r=e.length;n=e?t.push(r):r.startTime===r.endTime&&r.startTime<=e&&r.startTime+.5>=e&&t.push(r)}if(o=!1,t.length!==this.activeCues_.length)o=!0;else for(var a=0;a","‎":"‎","‏":"‏"," ":" "},Fn={c:"span",i:"i",b:"b",u:"u",ruby:"ruby",rt:"rt",v:"span",lang:"span"},Vn={v:"title",lang:"lang"},Hn={rt:"ruby"};function qn(a,i){function e(){if(!i)return null;var e,t=i.match(/^([^<]*)(<[^>]*>?)?/);return e=t[1]?t[1]:t[2],i=i.substr(e.length),e}function t(e){return jn[e]}function n(e){for(;f=e.match(/&(amp|lt|gt|lrm|rlm|nbsp);/);)e=e.replace(f[0],t);return e}function r(e,t){var i=Fn[e];if(!i)return null;var n=a.document.createElement(i);n.localName=i;var r=Vn[e];return r&&t&&(n[r]=t.trim()),n}for(var s,o,u,l=a.document.createElement("div"),c=l,h=[];null!==(s=e());)if("<"!==s[0])c.appendChild(a.document.createTextNode(n(s)));else{if("/"===s[1]){h.length&&h[h.length-1]===s.substr(2).replace(">","")&&(h.pop(),c=c.parentNode);continue}var d,p=Rn(s.substr(1,s.length-2));if(p){d=a.document.createProcessingInstruction("timestamp",p),c.appendChild(d);continue}var f=s.match(/^<([^.\s/0-9>]+)(\.[^\s\\>]+)?([^>\\]+)?(\\?)>?$/);if(!f)continue;if(!(d=r(f[1],f[3])))continue;if(o=c,Hn[(u=d).localName]&&Hn[u.localName]!==o.localName)continue;f[2]&&(d.className=f[2].substr(1).replace("."," ")),h.push(f[1]),c.appendChild(d),c=d}return l}var zn=[[1470,1470],[1472,1472],[1475,1475],[1478,1478],[1488,1514],[1520,1524],[1544,1544],[1547,1547],[1549,1549],[1563,1563],[1566,1610],[1645,1647],[1649,1749],[1765,1766],[1774,1775],[1786,1805],[1807,1808],[1810,1839],[1869,1957],[1969,1969],[1984,2026],[2036,2037],[2042,2042],[2048,2069],[2074,2074],[2084,2084],[2088,2088],[2096,2110],[2112,2136],[2142,2142],[2208,2208],[2210,2220],[8207,8207],[64285,64285],[64287,64296],[64298,64310],[64312,64316],[64318,64318],[64320,64321],[64323,64324],[64326,64449],[64467,64829],[64848,64911],[64914,64967],[65008,65020],[65136,65140],[65142,65276],[67584,67589],[67592,67592],[67594,67637],[67639,67640],[67644,67644],[67647,67669],[67671,67679],[67840,67867],[67872,67897],[67903,67903],[67968,68023],[68030,68031],[68096,68096],[68112,68115],[68117,68119],[68121,68147],[68160,68167],[68176,68184],[68192,68223],[68352,68405],[68416,68437],[68440,68466],[68472,68479],[68608,68680],[126464,126467],[126469,126495],[126497,126498],[126500,126500],[126503,126503],[126505,126514],[126516,126519],[126521,126521],[126523,126523],[126530,126530],[126535,126535],[126537,126537],[126539,126539],[126541,126543],[126545,126546],[126548,126548],[126551,126551],[126553,126553],[126555,126555],[126557,126557],[126559,126559],[126561,126562],[126564,126564],[126567,126570],[126572,126578],[126580,126583],[126585,126588],[126590,126590],[126592,126601],[126603,126619],[126625,126627],[126629,126633],[126635,126651],[1114109,1114109]];function Wn(e){for(var t=0;t=i[0]&&e<=i[1])return!0}return!1}function Gn(){}function $n(e,t,i){Gn.call(this),this.cue=t,this.cueDiv=qn(e,t.text);var n={color:"rgba(255, 255, 255, 1)",backgroundColor:"rgba(0, 0, 0, 0.8)",position:"relative",left:0,right:0,top:0,bottom:0,display:"inline",writingMode:""===t.vertical?"horizontal-tb":"lr"===t.vertical?"vertical-lr":"vertical-rl",unicodeBidi:"plaintext"};this.applyStyles(n,this.cueDiv),this.div=e.document.createElement("div"),n={direction:function(e){var t=[],i="";if(!e||!e.childNodes)return"ltr";function r(e,t){for(var i=t.childNodes.length-1;0<=i;i--)e.push(t.childNodes[i])}function a(e){if(!e||!e.length)return null;var t=e.pop(),i=t.textContent||t.innerText;if(i){var n=i.match(/^.*(\n|\r)/);return n?n[e.length=0]:i}return"ruby"===t.tagName?a(e):t.childNodes?(r(e,t),a(e)):void 0}for(r(t,e);i=a(t);)for(var n=0;nh&&(c=c<0?-1:1,c*=Math.ceil(h/l)*l),r<0&&(c+=""===n.vertical?o.height:o.width,a=a.reverse()),i.move(d,c)}else{var p=i.lineHeight/o.height*100;switch(n.lineAlign){case"middle":r-=p/2;break;case"end":r-=p}switch(n.vertical){case"":t.applyStyles({top:t.formatStyle(r,"%")});break;case"rl":t.applyStyles({left:t.formatStyle(r,"%")});break;case"lr":t.applyStyles({right:t.formatStyle(r,"%")})}a=["+y","-x","+x","-y"],i=new Xn(t)}var f=function(e,t){for(var i,n=new Xn(e),r=1,a=0;ae.left&&this.tope.top},Xn.prototype.overlapsAny=function(e){for(var t=0;t=e.top&&this.bottom<=e.bottom&&this.left>=e.left&&this.right<=e.right},Xn.prototype.overlapsOppositeAxis=function(e,t){switch(t){case"+x":return this.lefte.right;case"+y":return this.tope.bottom}},Xn.prototype.intersectPercentage=function(e){return Math.max(0,Math.min(this.right,e.right)-Math.max(this.left,e.left))*Math.max(0,Math.min(this.bottom,e.bottom)-Math.max(this.top,e.top))/(this.height*this.width)},Xn.prototype.toCSSCompatValues=function(e){return{top:this.top-e.top,bottom:e.bottom-this.bottom,left:this.left-e.left,right:e.right-this.right,height:this.height,width:this.width}},Xn.getSimpleBoxPosition=function(e){var t=e.div?e.div.offsetHeight:e.tagName?e.offsetHeight:0,i=e.div?e.div.offsetWidth:e.tagName?e.offsetWidth:0,n=e.div?e.div.offsetTop:e.tagName?e.offsetTop:0;return{left:(e=e.div?e.div.getBoundingClientRect():e.tagName?e.getBoundingClientRect():e).left,right:e.right,top:e.top||n,height:e.height||t,bottom:e.bottom||n+(e.height||t),width:e.width||i}},Yn.StringDecoder=function(){return{decode:function(e){if(!e)return"";if("string"!=typeof e)throw new Error("Error - expected string data.");return decodeURIComponent(encodeURIComponent(e))}}},Yn.convertCueToDOMTree=function(e,t){return e&&t?qn(e,t):null};Yn.processCues=function(n,r,e){if(!n||!r||!e)return null;for(;e.firstChild;)e.removeChild(e.firstChild);var a=n.document.createElement("div");if(a.style.position="absolute",a.style.left="0",a.style.right="0",a.style.top="0",a.style.bottom="0",a.style.margin="1.5%",e.appendChild(a),function(e){for(var t=0;t
- <%= translate(locale, "Current version: ") %> <%= CURRENT_VERSION %>-<%= CURRENT_COMMIT %> - - <%= CURRENT_BRANCH %> + <%= translate(locale, "Current version: ") %> <%= CURRENT_VERSION %>-<%= CURRENT_COMMIT %> @ <%= CURRENT_BRANCH %>
From 02fd02d4826c04e3253c57bdafc41ba00398318e Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Fri, 28 Feb 2020 11:46:24 -0500 Subject: [PATCH 0050/2930] Remove DB array concatenation --- src/invidious.cr | 12 ++++++------ src/invidious/channels.cr | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index d2ff2a3d..ea8cbcd2 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -468,7 +468,7 @@ get "/watch" do |env| env.params.query.delete_all("iv_load_policy") if watched && !watched.includes? id - PG_DB.exec("UPDATE users SET watched = watched || $1 WHERE email = $2", [id], user.as(User).email) + PG_DB.exec("UPDATE users SET watched = array_append(watched, $1) WHERE email = $2", id, user.as(User).email) end if notifications && notifications.includes? id @@ -748,7 +748,7 @@ get "/embed/:id" do |env| end # if watched && !watched.includes? id - # PG_DB.exec("UPDATE users SET watched = watched || $1 WHERE email = $2", [id], user.as(User).email) + # PG_DB.exec("UPDATE users SET watched = array_append(watched, $1) WHERE email = $2", id, user.as(User).email) # end if notifications && notifications.includes? id @@ -1243,11 +1243,11 @@ post "/playlist_ajax" do |env| args = arg_array(video_array) PG_DB.exec("INSERT INTO playlist_videos VALUES (#{args})", args: video_array) - PG_DB.exec("UPDATE playlists SET index = array_append(index, $1), video_count = video_count + 1, updated = $2 WHERE id = $3", playlist_video.index, Time.utc, playlist_id) + PG_DB.exec("UPDATE playlists SET index = array_append(index, $1), video_count = cardinality(index), updated = $2 WHERE id = $3", playlist_video.index, Time.utc, playlist_id) when "action_remove_video" index = env.params.query["set_video_id"] PG_DB.exec("DELETE FROM playlist_videos * WHERE index = $1", index) - PG_DB.exec("UPDATE playlists SET index = array_remove(index, $1), video_count = video_count - 1, updated = $2 WHERE id = $3", index, Time.utc, playlist_id) + PG_DB.exec("UPDATE playlists SET index = array_remove(index, $1), video_count = cardinality(index), updated = $2 WHERE id = $3", index, Time.utc, playlist_id) when "action_move_video_before" # TODO: Playlist stub end @@ -2244,7 +2244,7 @@ post "/watch_ajax" do |env| case action when "action_mark_watched" if !user.watched.includes? id - PG_DB.exec("UPDATE users SET watched = watched || $1 WHERE email = $2", [id], user.email) + PG_DB.exec("UPDATE users SET watched = array_append(watched, $1) WHERE email = $2", id, user.email) end when "action_mark_unwatched" PG_DB.exec("UPDATE users SET watched = array_remove(watched, $1) WHERE email = $2", id, user.email) @@ -3402,7 +3402,7 @@ post "/feed/webhook/:token" do |env| views: video.views, ) - emails = PG_DB.query_all("UPDATE users SET notifications = notifications || $1 \ + emails = PG_DB.query_all("UPDATE users SET notifications = array_append(notifications, $1) \ WHERE updated < $2 AND $3 = ANY(subscriptions) AND $1 <> ALL(notifications) RETURNING email", video.id, video.published, video.ucid, as: String) diff --git a/src/invidious/channels.cr b/src/invidious/channels.cr index cddeed39..e5cfb10f 100644 --- a/src/invidious/channels.cr +++ b/src/invidious/channels.cr @@ -273,7 +273,7 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil) views: views, ) - emails = db.query_all("UPDATE users SET notifications = notifications || $1 \ + emails = db.query_all("UPDATE users SET notifications = array_append(notifications, $1) \ WHERE updated < $2 AND $3 = ANY(subscriptions) AND $1 <> ALL(notifications) RETURNING email", video.id, video.published, ucid, as: String) @@ -342,7 +342,7 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil) # We are notified of Red videos elsewhere (PubSub), which includes a correct published date, # so since they don't provide a published date here we can safely ignore them. if Time.utc - video.published > 1.minute - emails = db.query_all("UPDATE users SET notifications = notifications || $1 \ + emails = db.query_all("UPDATE users SET notifications = array_append(notifications, $1) \ WHERE updated < $2 AND $3 = ANY(subscriptions) AND $1 <> ALL(notifications) RETURNING email", video.id, video.published, video.ucid, as: String) From 1caf6a32985338cccd4f189f924d60a3d4aa6cf8 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Fri, 28 Feb 2020 13:13:48 -0500 Subject: [PATCH 0051/2930] Fix deadlock when updating notifications --- src/invidious.cr | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index ea8cbcd2..0c80f8e0 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -3402,8 +3402,8 @@ post "/feed/webhook/:token" do |env| views: video.views, ) - emails = PG_DB.query_all("UPDATE users SET notifications = array_append(notifications, $1) \ - WHERE updated < $2 AND $3 = ANY(subscriptions) AND $1 <> ALL(notifications) RETURNING email", + PG_DB.query_all("UPDATE users SET feed_needs_update = true, notifications = array_append(notifications, $1) \ + WHERE updated < $2 AND $3 = ANY(subscriptions) AND $1 <> ALL(notifications)", video.id, video.published, video.ucid, as: String) video_array = video.to_a @@ -3413,15 +3413,6 @@ post "/feed/webhook/:token" do |env| ON CONFLICT (id) DO UPDATE SET title = $2, published = $3, \ updated = $4, ucid = $5, author = $6, length_seconds = $7, \ live_now = $8, premiere_timestamp = $9, views = $10", args: video_array) - - # Update all users affected by insert - if emails.empty? - values = "'{}'" - else - values = "VALUES #{emails.map { |email| %((E'#{email.gsub({'\'' => "\\'", '\\' => "\\\\"})}')) }.join(",")}" - end - - PG_DB.exec("UPDATE users SET feed_needs_update = true WHERE email = ANY(#{values})") end end From 697c00dccf02730ed1af2d71595af0177ac1baad Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Fri, 28 Feb 2020 14:10:01 -0500 Subject: [PATCH 0052/2930] Sanitize PLID --- src/invidious.cr | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index 0c80f8e0..0a10027b 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -428,7 +428,7 @@ get "/watch" do |env| next env.redirect "/" end - plid = env.params.query["list"]? + plid = env.params.query["list"]?.try &.gsub(/[^a-zA-Z0-9_-]/, "") continuation = process_continuation(PG_DB, env.params.query, plid, id) nojs = env.params.query["nojs"]? @@ -613,7 +613,7 @@ end get "/embed/" do |env| locale = LOCALES[env.get("preferences").as(Preferences).locale]? - if plid = env.params.query["list"]? + if plid = env.params.query["list"]?.try &.gsub(/[^a-zA-Z0-9_-]/, "") begin playlist = get_playlist(PG_DB, plid, locale: locale) offset = env.params.query["index"]?.try &.to_i? || 0 @@ -640,7 +640,7 @@ get "/embed/:id" do |env| locale = LOCALES[env.get("preferences").as(Preferences).locale]? id = env.params.url["id"] - plid = env.params.query["list"]? + plid = env.params.query["list"]?.try &.gsub(/[^a-zA-Z0-9_-]/, "") continuation = process_continuation(PG_DB, env.params.query, plid, id) if md = env.params.query["playlist"]? @@ -1264,9 +1264,9 @@ get "/playlist" do |env| locale = LOCALES[env.get("preferences").as(Preferences).locale]? user = env.get?("user").try &.as(User) - plid = env.params.query["list"]? referer = get_referer(env) + plid = env.params.query["list"]?.try &.gsub(/[^a-zA-Z0-9_-]/, "") if !plid next env.redirect "/" end From e21f77048579bc42f52d462bd3d41a68312fb157 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Fri, 28 Feb 2020 15:57:45 -0500 Subject: [PATCH 0053/2930] Fix status check for channel page --- src/invidious/channels.cr | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/invidious/channels.cr b/src/invidious/channels.cr index e5cfb10f..35ef5df2 100644 --- a/src/invidious/channels.cr +++ b/src/invidious/channels.cr @@ -556,11 +556,11 @@ end # TODO: Add "sort_by" def fetch_channel_community(ucid, continuation, locale, config, kemal_config, format, thin_mode) response = YT_POOL.client &.get("/channel/#{ucid}/community?gl=US&hl=en") - if response.status_code == 404 + if response.status_code != 200 response = YT_POOL.client &.get("/user/#{ucid}/community?gl=US&hl=en") end - if response.status_code == 404 + if response.status_code != 200 error_message = translate(locale, "This channel does not exist.") raise error_message end @@ -845,7 +845,7 @@ end def get_about_info(ucid, locale) about = YT_POOL.client &.get("/channel/#{ucid}/about?disable_polymer=1&gl=US&hl=en") - if about.status_code == 404 + if about.status_code != 200 about = YT_POOL.client &.get("/user/#{ucid}/about?disable_polymer=1&gl=US&hl=en") end @@ -853,6 +853,11 @@ def get_about_info(ucid, locale) raise ChannelRedirect.new(channel_id: md["ucid"]) end + if about.status_code != 200 + error_message = translate(locale, "This channel does not exist.") + raise error_message + end + about = XML.parse_html(about.body) if about.xpath_node(%q(//div[contains(@class, "channel-empty-message")])) From 9de57021a371cdbf87a3a758b88ecd3c945eeea9 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Sun, 1 Mar 2020 10:30:55 -0500 Subject: [PATCH 0054/2930] Update postgres setup --- README.md | 3 +++ docker/entrypoint.postgres.sh | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6f6e33dc..bc4b7b28 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,9 @@ $ psql invidious kemal < config/sql/users.sql $ psql invidious kemal < config/sql/session_ids.sql $ psql invidious kemal < config/sql/nonces.sql $ psql invidious kemal < config/sql/annotations.sql +$ psql invidious kemal < config/sql/privacy.sql +$ psql invidious kemal < config/sql/playlists.sql +$ psql invidious kemal < config/sql/playlist_videos.sql # Setup Invidious $ shards update && shards install diff --git a/docker/entrypoint.postgres.sh b/docker/entrypoint.postgres.sh index f5fce309..1588c56c 100755 --- a/docker/entrypoint.postgres.sh +++ b/docker/entrypoint.postgres.sh @@ -19,9 +19,9 @@ if [ ! -f /var/lib/postgresql/data/setupFinished ]; then su postgres -c 'psql invidious kemal < config/sql/session_ids.sql' su postgres -c 'psql invidious kemal < config/sql/nonces.sql' su postgres -c 'psql invidious kemal < config/sql/annotations.sql' + su postgres -c 'psql invidious kemal < config/sql/privacy.sql' su postgres -c 'psql invidious kemal < config/sql/playlists.sql' su postgres -c 'psql invidious kemal < config/sql/playlist_videos.sql' - su postgres -c 'psql invidious kemal < config/sql/privacy.sql' touch /var/lib/postgresql/data/setupFinished echo "### invidious database setup finished" exit From efbbb6fd206a2c7efb7ff3416bc6b71052961649 Mon Sep 17 00:00:00 2001 From: Deleted User Date: Sun, 19 Jan 2020 16:51:13 +0000 Subject: [PATCH 0055/2930] Update German translation --- locales/de.json | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/locales/de.json b/locales/de.json index 2e214b76..acf82e8b 100644 --- a/locales/de.json +++ b/locales/de.json @@ -1,7 +1,7 @@ { "`x` subscribers": "`x` Abonnenten", "`x` videos": "`x` Videos", - "`x` playlists": "", + "`x` playlists": "`x` Wiedergabelisten", "LIVE": "LIVE", "Shared `x` ago": "Vor `x` geteilt", "Unsubscribe": "Abbestellen", @@ -127,17 +127,17 @@ "View JavaScript license information.": "Javascript Lizenzinformationen anzeigen.", "View privacy policy.": "Datenschutzerklärung einsehen.", "Trending": "Trending", - "Public": "", + "Public": "Öffentlich", "Unlisted": "Nicht aufgeführt", - "Private": "", - "View all playlists": "", - "Updated `x` ago": "", - "Delete playlist `x`?": "", - "Delete playlist": "", - "Create playlist": "", - "Title": "", - "Playlist privacy": "", - "Editing playlist `x`": "", + "Private": "Privat", + "View all playlists": "Alle Wiedergabelisten anzeigen", + "Updated `x` ago": "Aktualisiert `x` vor", + "Delete playlist `x`?": "Wiedergabeliste löschen `x`?", + "Delete playlist": "Wiedergabeliste löschen", + "Create playlist": "Wiedergabeliste erstellen", + "Title": "Titel", + "Playlist privacy": "Vertrauliche Wiedergabeliste", + "Editing playlist `x`": "Wiedergabeliste bearbeiten `x`", "Watch on YouTube": "Video auf YouTube ansehen", "Hide annotations": "Anmerkungen ausblenden", "Show annotations": "Anmerkungen anzeigen", @@ -333,4 +333,4 @@ "Playlists": "Wiedergabelisten", "Community": "Gemeinschaft", "Current version: ": "Aktuelle Version: " -} \ No newline at end of file +} From 28554235be8cf70d7bbe3e525b092798edb6b94e Mon Sep 17 00:00:00 2001 From: Tymofij Lytvynenko Date: Thu, 20 Feb 2020 00:13:15 +0000 Subject: [PATCH 0056/2930] Update Ukrainian translation --- locales/uk.json | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/locales/uk.json b/locales/uk.json index 0f8aa1b6..5679949f 100644 --- a/locales/uk.json +++ b/locales/uk.json @@ -1,7 +1,7 @@ { "`x` subscribers": "`x` підписників", "`x` videos": "`x` відео", - "`x` playlists": "", + "`x` playlists": "списки відтворення \"x\"", "LIVE": "ПРЯМИЙ ЕФІР", "Shared `x` ago": "Розміщено `x` назад", "Unsubscribe": "Відписатися", @@ -69,11 +69,11 @@ "Show related videos: ": "Показувати схожі відео? ", "Show annotations by default: ": "Завжди показувати анотації? ", "Visual preferences": "Налаштування сайту", - "Player style: ": "", + "Player style: ": "Стиль програвача: ", "Dark mode: ": "Темне оформлення: ", - "Theme: ": "", - "dark": "", - "light": "", + "Theme: ": "Тема: ", + "dark": "темна", + "light": "Світла", "Thin mode: ": "Полегшене оформлення: ", "Subscription preferences": "Налаштування підписок", "Show annotations by default for subscribed channels: ": "Завжди показувати анотації у відео каналів, на які ви підписані? ", @@ -127,17 +127,17 @@ "View JavaScript license information.": "Переглянути інформацію щодо ліцензії JavaScript.", "View privacy policy.": "Переглянути політику приватності.", "Trending": "У тренді", - "Public": "", + "Public": "Прилюдний", "Unlisted": "Немає в списку", - "Private": "", - "View all playlists": "", - "Updated `x` ago": "", - "Delete playlist `x`?": "", - "Delete playlist": "", - "Create playlist": "", - "Title": "", - "Playlist privacy": "", - "Editing playlist `x`": "", + "Private": "Особистий", + "View all playlists": "Переглянути всі списки відтворення", + "Updated `x` ago": "Оновлено `x` тому", + "Delete playlist `x`?": "Видалити список відтворення \"x\"?", + "Delete playlist": "Видалити список відтворення", + "Create playlist": "Створити список відтворення", + "Title": "Заголовок", + "Playlist privacy": "Конфіденційність списку відтворення", + "Editing playlist `x`": "Редагування списку відтворення \"x\"", "Watch on YouTube": "Дивитися на YouTube", "Hide annotations": "Приховати анотації", "Show annotations": "Показати анотації", @@ -333,4 +333,4 @@ "Playlists": "Плейлисти", "Community": "", "Current version: ": "Поточна версія: " -} \ No newline at end of file +} From ebd46914623ed0b2ede73e0af6623b0f0f3345df Mon Sep 17 00:00:00 2001 From: Karol Kosek Date: Thu, 20 Feb 2020 23:34:09 +0000 Subject: [PATCH 0057/2930] Update Polish translation --- locales/pl.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/pl.json b/locales/pl.json index 36e739db..1ba6f942 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -19,7 +19,7 @@ "New passwords must match": "Nowe hasła muszą być identyczne", "Cannot change password for Google accounts": "Nie można zmienić hasła do konta Google", "Authorize token?": "Autoryzować token?", - "Authorize token for `x`?": "", + "Authorize token for `x`?": "Autoryzować token dla `x`?", "Yes": "Tak", "No": "Nie", "Import and Export Data": "Import i eksport danych", From ef70668a77c17cafa47776dde284f94faa957364 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Sun, 1 Mar 2020 10:51:17 -0500 Subject: [PATCH 0058/2930] Add HOST_AUTH_METHOD=trust to docker compose (see docker-library/postgres#681) --- docker-compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 1486f013..f96cd69d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,6 +7,8 @@ services: restart: unless-stopped volumes: - postgresdata:/var/lib/postgresql/data + environment: + - POSTGRES_HOST_AUTH_METHOD=trust healthcheck: test: ["CMD", "pg_isready", "-U", "postgres"] invidious: From c80c5631f0483c32c3b86bad3584c349a61b4b92 Mon Sep 17 00:00:00 2001 From: leonklingele Date: Sun, 1 Mar 2020 17:06:45 +0100 Subject: [PATCH 0059/2930] docker: do not require password for PostgreSQL superuser, docker,kubernetes: create "privacy" type before using it, travis: do not run "docker-compose up" in detached mode (#1042) * docker: do not require password for PostgreSQL superuser A password is now required by the postgres Docker image which makes initial setup (and our CI build) fail with the following error: postgres_1 | Error: Database is uninitialized and superuser password is not specified. postgres_1 | You must specify POSTGRES_PASSWORD for the superuser. Use postgres_1 | "-e POSTGRES_PASSWORD=password" to set it in "docker run". postgres_1 | postgres_1 | You may also use POSTGRES_HOST_AUTH_METHOD=trust to allow all connections postgres_1 | without a password. This is *not* recommended. See PostgreSQL postgres_1 | documentation about "trust": postgres_1 | https://www.postgresql.org/docs/current/auth-trust.html See https://github.com/docker-library/postgres/issues/681. * docker,kubernetes: create PostgreSQL "privacy" type before using it Fixes the following error when setting up the database: postgres_1 | 2020-02-21 01:01:22.371 UTC [172] ERROR: type "privacy" does not exist at character 200 postgres_1 | 2020-02-21 01:01:22.371 UTC [172] STATEMENT: CREATE TABLE public.playlists postgres_1 | ( postgres_1 | title text, postgres_1 | id text primary key, postgres_1 | author text, postgres_1 | description text, postgres_1 | video_count integer, postgres_1 | created timestamptz, postgres_1 | updated timestamptz, postgres_1 | privacy privacy, postgres_1 | index int8[] postgres_1 | ); postgres_1 | ERROR: type "privacy" does not exist postgres_1 | LINE 10: privacy privacy, * travis: do not run "docker-compose up" in detached mode Rather, allow database to finish its setup procedure and grant Invidious time to launch. --- .travis.yml | 2 +- docker/Dockerfile.postgres | 3 +++ kubernetes/README.md | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 314abc73..8b83db2a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,7 +27,7 @@ jobs: install: - docker-compose build script: - - docker-compose up -d + - docker-compose up - sleep 15 # Wait for cluster to become ready, TODO: do not sleep - HEADERS="$(curl -I -s http://localhost:3000/)" - STATUS="$(echo $HEADERS | head -n1)" diff --git a/docker/Dockerfile.postgres b/docker/Dockerfile.postgres index 720bdff8..3b25b802 100644 --- a/docker/Dockerfile.postgres +++ b/docker/Dockerfile.postgres @@ -1,6 +1,9 @@ FROM postgres:10 ENV POSTGRES_USER postgres +# Do not require a PostgreSQL superuser password. +# See https://github.com/docker-library/postgres/issues/681. +ENV POSTGRES_HOST_AUTH_METHOD trust ADD ./config/sql /config/sql ADD ./docker/entrypoint.postgres.sh /entrypoint.sh diff --git a/kubernetes/README.md b/kubernetes/README.md index 163e9cd7..1c62f469 100644 --- a/kubernetes/README.md +++ b/kubernetes/README.md @@ -17,9 +17,9 @@ $ kubectl create configmap invidious-postgresql-init \ --from-file=../config/sql/session_ids.sql \ --from-file=../config/sql/nonces.sql \ --from-file=../config/sql/annotations.sql \ + --from-file=../config/sql/privacy.sql \ --from-file=../config/sql/playlists.sql \ - --from-file=../config/sql/playlist_videos.sql \ - --from-file=../config/sql/privacy.sql + --from-file=../config/sql/playlist_videos.sql # Install Helm app to your Kubernetes cluster $ helm install invidious ./ From 856ec03cc7641e8e39db42690e04bfbd11032afd Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Sun, 1 Mar 2020 11:07:20 -0500 Subject: [PATCH 0060/2930] Revert "Add HOST_AUTH_METHOD=trust to docker compose (see docker-library/postgres#681)" This reverts commit ef70668a77c17cafa47776dde284f94faa957364. --- docker-compose.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index f96cd69d..1486f013 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,8 +7,6 @@ services: restart: unless-stopped volumes: - postgresdata:/var/lib/postgresql/data - environment: - - POSTGRES_HOST_AUTH_METHOD=trust healthcheck: test: ["CMD", "pg_isready", "-U", "postgres"] invidious: From e3593fe197369c583fc6f91292ff8cc06f87eced Mon Sep 17 00:00:00 2001 From: Leon Klingele Date: Mon, 19 Aug 2019 12:10:25 +0200 Subject: [PATCH 0061/2930] js: add support to detect media keys in keydown handler See [0] for all the relevant codes. [0]: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values#Multimedia_keys Fixes a regression introduced in e6b4e1268945777c5d07dfca4362a1af23f6d970. Fixes https://github.com/omarroth/invidious/issues/712. --- assets/js/player.js | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/assets/js/player.js b/assets/js/player.js index eecc0868..dc1e633f 100644 --- a/assets/js/player.js +++ b/assets/js/player.js @@ -228,11 +228,24 @@ function set_time_percent(percent) { player.currentTime(newTime); } +function play() { + player.play(); +} + +function pause() { + player.pause(); +} + +function stop() { + player.pause(); + player.currentTime(0); +} + function toggle_play() { if (player.paused()) { - player.play(); + play(); } else { - player.pause(); + pause(); } } @@ -338,9 +351,22 @@ window.addEventListener('keydown', e => { switch (decoratedKey) { case ' ': case 'k': + case 'MediaPlayPause': action = toggle_play; break; + case 'MediaPlay': + action = play; + break; + + case 'MediaPause': + action = pause; + break; + + case 'MediaStop': + action = stop; + break; + case 'ArrowUp': if (isPlayerFocused) { action = increase_volume.bind(this, 0.1); @@ -357,9 +383,11 @@ window.addEventListener('keydown', e => { break; case 'ArrowRight': + case 'MediaFastForward': action = skip_seconds.bind(this, 5); break; case 'ArrowLeft': + case 'MediaTrackPrevious': action = skip_seconds.bind(this, -5); break; case 'l': @@ -391,9 +419,11 @@ window.addEventListener('keydown', e => { break; case 'N': + case 'MediaTrackNext': action = next_video; break; case 'P': + case 'MediaTrackPrevious': // TODO: Add support to play back previous video. break; From c620a22017012a0dfcd0c3c89017a17587fe7b0e Mon Sep 17 00:00:00 2001 From: Tommy Miland Date: Mon, 2 Mar 2020 16:19:07 +0100 Subject: [PATCH 0062/2930] Add logfile to logrotate (#892) --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index bc4b7b28..29e359bf 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,20 @@ $ sudo systemctl enable invidious.service $ sudo systemctl start invidious.service ``` +#### Logrotate + +```bash +$ sudo echo "/home/invidious/invidious/invidious.log { +rotate 4 +weekly +notifempty +missingok +compress +minsize 1048576 +}" | tee /etc/logrotate.d/invidious.logrotate +$ sudo chmod 0644 /etc/logrotate.d/invidious.logrotate +``` + ### OSX: ```bash From a3045a3953c5446887ae2057383023bf35c26253 Mon Sep 17 00:00:00 2001 From: Kyle Copperfield Date: Mon, 2 Mar 2020 15:33:47 +0000 Subject: [PATCH 0063/2930] Use a MediaQueryListener to toggle on demand. Tested on OSX. (#925) Closes #867. --- assets/js/themes.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/assets/js/themes.js b/assets/js/themes.js index 90a05c36..c600073d 100644 --- a/assets/js/themes.js +++ b/assets/js/themes.js @@ -28,6 +28,27 @@ window.addEventListener('load', function () { update_mode(window.localStorage.dark_mode); }); + +var darkScheme = window.matchMedia('(prefers-color-scheme: dark)'); +var lightScheme = window.matchMedia('(prefers-color-scheme: light)'); + +darkScheme.addListener(scheme_switch); +lightScheme.addListener(scheme_switch); + +function scheme_switch (e) { + // ignore this method if we have a preference set + if (localStorage.getItem('dark_mode')) { + return; + } + if (e.matches) { + if (e.media.includes("dark")) { + set_mode(true); + } else if (e.media.includes("light")) { + set_mode(false); + } + } +} + function set_mode (bool) { document.getElementById('dark_theme').media = !bool ? 'none' : ''; document.getElementById('light_theme').media = bool ? 'none' : ''; From 72a4962fd00076189270daefb1e217d9d68fc50e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=CD=85=CD=88=CC=A4=CC=96=CC=AA=CC=AA=CC=B1l=CC=AF=CC=AF?= =?UTF-8?q?=CC=AE=CC=BC=CD=8E=CC=AC=CD=9A=CC=B3=CC=A9=CC=96=CC=B2u=CC=9C?= =?UTF-8?q?=CC=BC=CD=89=CD=88=CC=A0b=CD=99=CC=AC=CC=98=CC=99=CC=B1=CC=97?= =?UTF-8?q?=CC=B2=CD=99b=CD=8D=CC=9E=CC=AC=CC=AC=CD=93=CC=BCl=CC=B0=CC=AA?= =?UTF-8?q?=CD=96=CC=AF=CC=BC=CC=9F=CD=85=CC=9F=CD=88=CC=96=CD=95=CC=9C?= =?UTF-8?q?=CC=B1=CC=9Cl=CC=BB=CC=97=CD=94=CC=9D=CC=AD=CC=B0=CD=9A=CD=87?= =?UTF-8?q?=CC=AF=CC=A5?= <34196146+blubbll@users.noreply.github.com> Date: Mon, 2 Mar 2020 16:35:28 +0100 Subject: [PATCH 0064/2930] add lapisTube (#1027) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 29e359bf..3d453208 100644 --- a/README.md +++ b/README.md @@ -229,6 +229,7 @@ $ ./sentry - [CloudTube](https://cadence.moe/cloudtube/subscriptions): A JS-rich alternate YouTube player - [PeerTubeify](https://gitlab.com/Ealhad/peertubeify): On YouTube, displays a link to the same video on PeerTube, if it exists. - [MusicPiped](https://github.com/deep-gaurav/MusicPiped): A materialistic music player that streams music from YouTube. +- [LapisTube](https://github.com/blubbll/lapis-tube): A fancy and advanced (experimental) YouTube frontend. Combined streams & custom YT features. ## Contributing From 0d536d11e3d816802f4e6c569ef56d43140710aa Mon Sep 17 00:00:00 2001 From: leonklingele Date: Mon, 2 Mar 2020 17:04:36 +0100 Subject: [PATCH 0065/2930] Verify token signature in constant time, Run cheap checks first in token validation process (#1032) * Verify token signature in constant time To prevent timing side channel attacks * Run cheap checks first in token validation process Expensive checks such as the nonce lookup on the database or the signature check can be run after cheap/fast checks. --- src/invidious/helpers/tokens.cr | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/invidious/helpers/tokens.cr b/src/invidious/helpers/tokens.cr index 30f7d4f4..0b609e80 100644 --- a/src/invidious/helpers/tokens.cr +++ b/src/invidious/helpers/tokens.cr @@ -1,3 +1,5 @@ +require "crypto/subtle" + def generate_token(email, scopes, expire, key, db) session = "v1:#{Base64.urlsafe_encode(Random::Secure.random_bytes(32))}" PG_DB.exec("INSERT INTO session_ids VALUES ($1, $2, $3)", session, email, Time.utc) @@ -76,14 +78,25 @@ def validate_request(token, session, request, key, db, locale = nil) raise translate(locale, "Hidden field \"token\" is a required field") end - if token["signature"] != sign_token(key, token) - raise translate(locale, "Invalid signature") + expire = token["expire"]?.try &.as_i + if expire.try &.< Time.utc.to_unix + raise translate(locale, "Token is expired, please try again") end if token["session"] != session raise translate(locale, "Erroneous token") end + scopes = token["scopes"].as_a.map { |v| v.as_s } + scope = "#{request.method}:#{request.path.lchop("/api/v1/auth/").lstrip("/")}" + if !scopes_include_scope(scopes, scope) + raise translate(locale, "Invalid scope") + end + + if !Crypto::Subtle.constant_time_compare(token["signature"].to_s, sign_token(key, token)) + raise translate(locale, "Invalid signature") + end + if token["nonce"]? && (nonce = db.query_one?("SELECT * FROM nonces WHERE nonce = $1", token["nonce"], as: {String, Time})) if nonce[1] > Time.utc db.exec("UPDATE nonces SET expire = $1 WHERE nonce = $2", Time.utc(1990, 1, 1), nonce[0]) @@ -92,18 +105,6 @@ def validate_request(token, session, request, key, db, locale = nil) end end - scopes = token["scopes"].as_a.map { |v| v.as_s } - scope = "#{request.method}:#{request.path.lchop("/api/v1/auth/").lstrip("/")}" - - if !scopes_include_scope(scopes, scope) - raise translate(locale, "Invalid scope") - end - - expire = token["expire"]?.try &.as_i - if expire.try &.< Time.utc.to_unix - raise translate(locale, "Token is expired, please try again") - end - return {scopes, expire, token["signature"].as_s} end From 9dc4f8a1aa7ac183b2eadf73a5f0be35931e8ce3 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Wed, 4 Mar 2020 13:03:14 -0500 Subject: [PATCH 0066/2930] Escape item titles in search page --- src/invidious/views/components/item.ecr | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr index f7b9cce6..9669aaeb 100644 --- a/src/invidious/views/components/item.ecr +++ b/src/invidious/views/components/item.ecr @@ -44,7 +44,7 @@ <% end %> <% end %> -

<%= item.title %>

+

<%= HTML.escape(item.title) %>

@@ -76,7 +76,7 @@ <% end %> <% end %> -

<%= item.title %>

+

<%= HTML.escape(item.title) %>

@@ -137,7 +137,7 @@ <% end %> -

<%= item.title %>

+

<%= HTML.escape(item.title) %>

<%= item.author %> From a117d87f331607864452b85c6066d972ce2cb568 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Wed, 4 Mar 2020 13:05:10 -0500 Subject: [PATCH 0067/2930] Skip validation checks for videoplayback, ggpht --- src/invidious/helpers/utils.cr | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index e43ae71d..7c5edc5c 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -77,7 +77,8 @@ def elapsed_text(elapsed) end def make_client(url : URI, region = nil) - client = HTTPClient.new(url) + # TODO: Migrate any applicable endpoints to QUIC + client = HTTPClient.new(url, OpenSSL::SSL::Context::Client.insecure) client.family = (url.host == "www.youtube.com") ? CONFIG.force_resolve : Socket::Family::UNSPEC client.read_timeout = 10.seconds client.connect_timeout = 10.seconds From 3126e1ac949092a9597280d3b11103cedd4adeef Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Wed, 4 Mar 2020 12:33:13 -0600 Subject: [PATCH 0068/2930] docker: allow to configure Invidious by env var (#1030) Invidious gained support to read its configuration from an env var instead of config file in e3c10d779d315adc630e08005b6bdbdce32f7446. Unfortunately, Docker doesn't allow newline characters in env var values (see [0]) which means we can only provide a proper YAML config by using the inlined configuration in docker-compose.yml which, unfortunately, is tracked by Git. Once support for multiline env var values has been added to Docker, we should migrate and read the config from a .env file instead (which is not tracked by Git). [0]: https://github.com/docker/compose/issues/3527 --- docker-compose.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 1486f013..d7b3fa91 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,6 +16,20 @@ services: restart: unless-stopped ports: - "127.0.0.1:3000:3000" + environment: + # Adapted from ./config/config.yml + INVIDIOUS_CONFIG: | + channel_threads: 1 + feed_threads: 1 + db: + user: kemal + password: kemal + host: postgres + port: 5432 + dbname: invidious + full_refresh: false + https_only: false + domain: depends_on: - postgres From bd0aaa343b69d65607a34d2a82bae2da493d6cba Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Wed, 4 Mar 2020 15:36:39 -0500 Subject: [PATCH 0069/2930] Prevent storyboards from hanging --- src/invidious.cr | 1 + 1 file changed, 1 insertion(+) diff --git a/src/invidious.cr b/src/invidious.cr index 0a10027b..a066407f 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -5714,6 +5714,7 @@ get "/sb/:id/:storyboard/:index" do |env| end end + env.response.headers["Connection"] = "close" env.response.headers["Access-Control-Allow-Origin"] = "*" if response.status_code >= 300 From d96dee3aa66980cfdd4a18d728fa95d407797e36 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Fri, 6 Mar 2020 13:50:00 -0500 Subject: [PATCH 0070/2930] Add debug info to videoplayback --- src/invidious.cr | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index a066407f..1c810d1c 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -5482,8 +5482,8 @@ get "/videoplayback" do |env| end client = make_client(URI.parse(host), region) - response = HTTP::Client::Response.new(500) + error = "" 5.times do begin response = client.head(url, headers) @@ -5508,12 +5508,14 @@ get "/videoplayback" do |env| host = "https://r#{fvip}---#{mn}.googlevideo.com" client = make_client(URI.parse(host), region) rescue ex + error = ex.message end end if response.status_code >= 400 env.response.status_code = response.status_code - next + env.response.content_type = "text/plain" + next error end if url.includes? "&file=seg.ts" From bb72672dd90c0cd394891622637f99d828f1525c Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Fri, 6 Mar 2020 13:53:35 -0500 Subject: [PATCH 0071/2930] Replace static asset requests with QUIC --- src/invidious.cr | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index 1c810d1c..fe07f9b5 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -99,8 +99,7 @@ LOCALES = { "zh-TW" => load_locale("zh-TW"), } -YT_POOL = QUICPool.new(YT_URL, capacity: CONFIG.pool_size, timeout: 0.05) -YT_IMG_POOL = QUICPool.new(YT_IMG_URL, capacity: CONFIG.pool_size, timeout: 0.05) +YT_POOL = QUICPool.new(YT_URL, capacity: CONFIG.pool_size, timeout: 0.1) config = CONFIG logger = Invidious::LogHandler.new @@ -5646,11 +5645,10 @@ get "/videoplayback" do |env| end get "/ggpht/*" do |env| - host = "https://yt3.ggpht.com" - client = make_client(URI.parse(host)) url = env.request.path.lchop("/ggpht") headers = HTTP::Headers.new + headers[":authority"] = "yt3.ggpht.com" REQUEST_HEADERS_WHITELIST.each do |header| if env.request.headers[header]? headers[header] = env.request.headers[header] @@ -5658,7 +5656,7 @@ get "/ggpht/*" do |env| end begin - client.get(url, headers) do |response| + YT_POOL.client &.get(url, headers) do |response| env.response.status_code = response.status_code response.headers.each do |key, value| if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase) @@ -5691,16 +5689,16 @@ get "/sb/:id/:storyboard/:index" do |env| storyboard = env.params.url["storyboard"] index = env.params.url["index"] - if storyboard.starts_with? "storyboard_live" - host = "https://i.ytimg.com" - else - host = "https://i9.ytimg.com" - end - client = make_client(URI.parse(host)) - url = "/sb/#{id}/#{storyboard}/#{index}?#{env.params.query}" headers = HTTP::Headers.new + + if storyboard.starts_with? "storyboard_live" + headers[":authority"] = "i.ytimg.com" + else + headers[":authority"] = "i9.ytimg.com" + end + REQUEST_HEADERS_WHITELIST.each do |header| if env.request.headers[header]? headers[header] = env.request.headers[header] @@ -5708,7 +5706,7 @@ get "/sb/:id/:storyboard/:index" do |env| end begin - client.get(url, headers) do |response| + YT_POOL.client &.get(url, headers) do |response| env.response.status_code = response.status_code response.headers.each do |key, value| if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase) @@ -5734,11 +5732,10 @@ get "/s_p/:id/:name" do |env| id = env.params.url["id"] name = env.params.url["name"] - host = "https://i9.ytimg.com" - client = make_client(URI.parse(host)) url = env.request.resource headers = HTTP::Headers.new + headers[":authority"] = "i9.ytimg.com" REQUEST_HEADERS_WHITELIST.each do |header| if env.request.headers[header]? headers[header] = env.request.headers[header] @@ -5746,7 +5743,7 @@ get "/s_p/:id/:name" do |env| end begin - client.get(url, headers) do |response| + YT_POOL.client &.get(url, headers) do |response| env.response.status_code = response.status_code response.headers.each do |key, value| if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase) @@ -5801,9 +5798,12 @@ get "/vi/:id/:name" do |env| id = env.params.url["id"] name = env.params.url["name"] + headers = HTTP::Headers.new + headers[":authority"] = "i.ytimg.com" + if name == "maxres.jpg" build_thumbnails(id, config, Kemal.config).each do |thumb| - if YT_IMG_POOL.client &.head("/vi/#{id}/#{thumb[:url]}.jpg").status_code == 200 + if YT_POOL.client &.head("/vi/#{id}/#{thumb[:url]}.jpg", headers).status_code == 200 name = thumb[:url] + ".jpg" break end @@ -5811,7 +5811,6 @@ get "/vi/:id/:name" do |env| end url = "/vi/#{id}/#{name}" - headers = HTTP::Headers.new REQUEST_HEADERS_WHITELIST.each do |header| if env.request.headers[header]? headers[header] = env.request.headers[header] @@ -5819,7 +5818,7 @@ get "/vi/:id/:name" do |env| end begin - YT_IMG_POOL.client &.get(url, headers) do |response| + YT_POOL.client &.get(url, headers) do |response| env.response.status_code = response.status_code response.headers.each do |key, value| if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase) From 6ff2229a092aa143c0647061255cbd160cdf4d11 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Fri, 6 Mar 2020 13:59:42 -0500 Subject: [PATCH 0072/2930] Bump dependencies --- shard.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shard.yml b/shard.yml index e8b8d189..59f5607b 100644 --- a/shard.yml +++ b/shard.yml @@ -26,8 +26,8 @@ dependencies: version: ~> 0.1.2 lsquic: github: omarroth/lsquic.cr - version: ~> 0.1.8 + version: ~> 0.1.9 -crystal: 0.32.0 +crystal: 0.33.0 license: AGPLv3 From 1443335315651b5a2ea7dc320d20c7b0dee41b63 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Tue, 10 Mar 2020 11:12:11 -0400 Subject: [PATCH 0073/2930] Switch textcaptcha to HTTPS --- src/invidious.cr | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index fe07f9b5..e89c2c37 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -48,9 +48,8 @@ ARCHIVE_URL = URI.parse("https://archive.org") LOGIN_URL = URI.parse("https://accounts.google.com") PUBSUB_URL = URI.parse("https://pubsubhubbub.appspot.com") REDDIT_URL = URI.parse("https://www.reddit.com") -TEXTCAPTCHA_URL = URI.parse("http://textcaptcha.com") +TEXTCAPTCHA_URL = URI.parse("https://textcaptcha.com") YT_URL = URI.parse("https://www.youtube.com") -YT_IMG_URL = URI.parse("https://i.ytimg.com") CHARS_SAFE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" TEST_IDS = {"AgbeGFYluEA", "BaW_jenozKc", "a9LDPn-MO4I", "ddFvjfvPnqk", "iqKdEhx-dD4"} From f92027c44b043c19188ce9945c3f05e6dc90de5a Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Tue, 10 Mar 2020 11:25:32 -0400 Subject: [PATCH 0074/2930] Escape 'sort_by' --- src/invidious/views/channel.ecr | 4 ++-- src/invidious/views/playlists.ecr | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/invidious/views/channel.ecr b/src/invidious/views/channel.ecr index b5eb46ea..4e9c7a63 100644 --- a/src/invidious/views/channel.ecr +++ b/src/invidious/views/channel.ecr @@ -92,7 +92,7 @@

<% if page > 1 %> - &sort_by=<%= sort_by %><% end %>"> + &sort_by=<%= HTML.escape(sort_by) %><% end %>"> <%= translate(locale, "Previous page") %> <% end %> @@ -100,7 +100,7 @@
<% if count == 60 %> - &sort_by=<%= sort_by %><% end %>"> + &sort_by=<%= HTML.escape(sort_by) %><% end %>"> <%= translate(locale, "Next page") %> <% end %> diff --git a/src/invidious/views/playlists.ecr b/src/invidious/views/playlists.ecr index a32192b5..0c48be96 100644 --- a/src/invidious/views/playlists.ecr +++ b/src/invidious/views/playlists.ecr @@ -90,7 +90,7 @@
<% if continuation %> - &sort_by=<%= sort_by %><% end %>"> + &sort_by=<%= HTML.escape(sort_by) %><% end %>"> <%= translate(locale, "Next page") %> <% end %> From 70cbe91776d1de10f2767c6a5ad5912fd705bdd3 Mon Sep 17 00:00:00 2001 From: leonklingele Date: Mon, 16 Mar 2020 06:46:08 +0900 Subject: [PATCH 0075/2930] Migrate to a good Content Security Policy (#1023) So attacks such as XSS (see [0]) will no longer be of an issue. [0]: https://github.com/omarroth/invidious/issues/1022 --- assets/css/embed.css | 10 ++ assets/js/community.js | 2 + assets/js/embed.js | 2 + assets/js/global.js | 3 + assets/js/handlers.js | 141 ++++++++++++++++++ assets/js/notifications.js | 2 + assets/js/player.js | 3 + assets/js/playlist_widget.js | 2 + ...silvermine-videojs-quality-selector.min.js | 5 +- assets/js/subscribe_widget.js | 2 + assets/js/watch.js | 2 + assets/js/watched_widget.js | 2 + src/invidious.cr | 20 ++- src/invidious/comments.cr | 8 +- src/invidious/views/add_playlist_items.ecr | 6 +- src/invidious/views/community.ecr | 16 +- src/invidious/views/components/item.ecr | 15 +- src/invidious/views/components/player.ecr | 17 +-- .../views/components/player_sources.ecr | 1 + .../views/components/subscribe_widget.ecr | 16 +- src/invidious/views/embed.ecr | 33 ++-- src/invidious/views/history.ecr | 10 +- src/invidious/views/playlist.ecr | 6 +- src/invidious/views/preferences.ecr | 10 +- src/invidious/views/subscription_manager.ecr | 33 +--- src/invidious/views/subscriptions.ecr | 6 +- src/invidious/views/template.ecr | 9 +- src/invidious/views/token_manager.ecr | 33 +--- src/invidious/views/watch.ecr | 34 ++--- 29 files changed, 274 insertions(+), 175 deletions(-) create mode 100644 assets/css/embed.css create mode 100644 assets/js/global.js create mode 100644 assets/js/handlers.js diff --git a/assets/css/embed.css b/assets/css/embed.css new file mode 100644 index 00000000..12fefe58 --- /dev/null +++ b/assets/css/embed.css @@ -0,0 +1,10 @@ +#player { + position: fixed; + right: 0; + bottom: 0; + min-width: 100%; + min-height: 100%; + width: auto; + height: auto; + z-index: -100; +} diff --git a/assets/js/community.js b/assets/js/community.js index 754ec6d3..4077f1cd 100644 --- a/assets/js/community.js +++ b/assets/js/community.js @@ -1,3 +1,5 @@ +var community_data = JSON.parse(document.getElementById('community_data').innerHTML); + String.prototype.supplant = function (o) { return this.replace(/{([^{}]*)}/g, function (a, b) { var r = o[b]; diff --git a/assets/js/embed.js b/assets/js/embed.js index 534c30ff..99d2fc53 100644 --- a/assets/js/embed.js +++ b/assets/js/embed.js @@ -1,3 +1,5 @@ +var video_data = JSON.parse(document.getElementById('video_data').innerHTML); + function get_playlist(plid, retries) { if (retries == undefined) retries = 5; diff --git a/assets/js/global.js b/assets/js/global.js new file mode 100644 index 00000000..efb447fb --- /dev/null +++ b/assets/js/global.js @@ -0,0 +1,3 @@ +// Disable Web Workers. Fixes Video.js CSP violation (created by `new Worker(objURL)`): +// Refused to create a worker from 'blob:http://host/id' because it violates the following Content Security Policy directive: "worker-src 'self'". +window.Worker = undefined; diff --git a/assets/js/handlers.js b/assets/js/handlers.js new file mode 100644 index 00000000..68ba9f4f --- /dev/null +++ b/assets/js/handlers.js @@ -0,0 +1,141 @@ +'use strict'; + +(function() { + var n2a = function(n) { return Array.prototype.slice.call(n); }; + + var video_player = document.getElementById('player'); + if (video_player) { + video_player.onmouseenter = function() { video_player['data-title'] = video_player['title']; video_player['title'] = ''; }; + video_player.onmouseleave = function() { video_player['title'] = video_player['data-title']; video_player['data-title'] = ''; }; + video_player.oncontextmenu = function() { video_player['title'] = video_player['data-title']; }; + } + + // For dynamically inserted elements + document.addEventListener('click', function(e) { + if (!e || !e.target) { return; } + e = e.target; + var handler_name = e.getAttribute('data-onclick'); + switch (handler_name) { + case 'jump_to_time': + var time = e.getAttribute('data-jump-time'); + player.currentTime(time); + break; + case 'get_youtube_replies': + var load_more = e.getAttribute('data-load-more') !== null; + get_youtube_replies(e, load_more); + break; + default: + break; + } + }); + + n2a(document.querySelectorAll('[data-mouse="switch_classes"]')).forEach(function(e) { + var classes = e.getAttribute('data-switch-classes').split(','); + var ec = classes[0]; + var lc = classes[1]; + var onoff = function(on, off) { + var cs = e.getAttribute('class'); + cs = cs.split(off).join(on); + e.setAttribute('class', cs); + }; + e.onmouseenter = function() { onoff(ec, lc); }; + e.onmouseleave = function() { onoff(lc, ec); }; + }); + + n2a(document.querySelectorAll('[data-onsubmit="return_false"]')).forEach(function(e) { + e.onsubmit = function() { return false; }; + }); + + n2a(document.querySelectorAll('[data-onclick="toggle_parent"]')).forEach(function(e) { + e.onclick = function() { toggle_parent(e); }; + }); + n2a(document.querySelectorAll('[data-onclick="mark_watched"]')).forEach(function(e) { + e.onclick = function() { mark_watched(e); }; + }); + n2a(document.querySelectorAll('[data-onclick="mark_unwatched"]')).forEach(function(e) { + e.onclick = function() { mark_unwatched(e); }; + }); + n2a(document.querySelectorAll('[data-onclick="add_playlist_item"]')).forEach(function(e) { + e.onclick = function() { add_playlist_item(e); }; + }); + n2a(document.querySelectorAll('[data-onclick="remove_playlist_item"]')).forEach(function(e) { + e.onclick = function() { remove_playlist_item(e); }; + }); + n2a(document.querySelectorAll('[data-onclick="revoke_token"]')).forEach(function(e) { + e.onclick = function() { revoke_token(e); }; + }); + n2a(document.querySelectorAll('[data-onclick="remove_subscription"]')).forEach(function(e) { + e.onclick = function() { remove_subscription(e); }; + }); + n2a(document.querySelectorAll('[data-onclick="notification_requestPermission"]')).forEach(function(e) { + e.onclick = function() { Notification.requestPermission(); }; + }); + + n2a(document.querySelectorAll('[data-onrange="update_volume_value"]')).forEach(function(e) { + var cb = function() { update_volume_value(e); } + e.oninput = cb; + e.onchange = cb; + }); + + function update_volume_value(element) { + document.getElementById('volume-value').innerText = element.value; + } + + function revoke_token(target) { + var row = target.parentNode.parentNode.parentNode.parentNode.parentNode; + row.style.display = 'none'; + var count = document.getElementById('count'); + count.innerText = count.innerText - 1; + + var referer = window.encodeURIComponent(document.location.href); + var url = '/token_ajax?action_revoke_token=1&redirect=false' + + '&referer=' + referer + + '&session=' + target.getAttribute('data-session'); + var xhr = new XMLHttpRequest(); + xhr.responseType = 'json'; + xhr.timeout = 10000; + xhr.open('POST', url, true); + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + + xhr.onreadystatechange = function() { + if (xhr.readyState == 4) { + if (xhr.status != 200) { + count.innerText = parseInt(count.innerText) + 1; + row.style.display = ''; + } + } + } + + var csrf_token = target.parentNode.querySelector('input[name="csrf_token"]').value; + xhr.send('csrf_token=' + csrf_token); + } + + function remove_subscription(target) { + var row = target.parentNode.parentNode.parentNode.parentNode.parentNode; + row.style.display = 'none'; + var count = document.getElementById('count'); + count.innerText = count.innerText - 1; + + var referer = window.encodeURIComponent(document.location.href); + var url = '/subscription_ajax?action_remove_subscriptions=1&redirect=false' + + '&referer=' + referer + + '&c=' + target.getAttribute('data-ucid'); + var xhr = new XMLHttpRequest(); + xhr.responseType = 'json'; + xhr.timeout = 10000; + xhr.open('POST', url, true); + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + + xhr.onreadystatechange = function() { + if (xhr.readyState == 4) { + if (xhr.status != 200) { + count.innerText = parseInt(count.innerText) + 1; + row.style.display = ''; + } + } + } + + var csrf_token = target.parentNode.querySelector('input[name="csrf_token"]').value; + xhr.send('csrf_token=' + csrf_token); + } +})(); diff --git a/assets/js/notifications.js b/assets/js/notifications.js index fcfc01e7..3d1ec1ed 100644 --- a/assets/js/notifications.js +++ b/assets/js/notifications.js @@ -1,3 +1,5 @@ +var notification_data = JSON.parse(document.getElementById('notification_data').innerHTML); + var notifications, delivered; function get_subscriptions(callback, retries) { diff --git a/assets/js/player.js b/assets/js/player.js index eecc0868..75370de6 100644 --- a/assets/js/player.js +++ b/assets/js/player.js @@ -1,3 +1,6 @@ +var player_data = JSON.parse(document.getElementById('player_data').innerHTML); +var video_data = JSON.parse(document.getElementById('video_data').innerHTML); + var options = { preload: 'auto', liveui: true, diff --git a/assets/js/playlist_widget.js b/assets/js/playlist_widget.js index 5d6ddf87..a29d7ef0 100644 --- a/assets/js/playlist_widget.js +++ b/assets/js/playlist_widget.js @@ -1,3 +1,5 @@ +var playlist_data = JSON.parse(document.getElementById('playlist_data').innerHTML); + function add_playlist_item(target) { var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode; tile.style.display = 'none'; diff --git a/assets/js/silvermine-videojs-quality-selector.min.js b/assets/js/silvermine-videojs-quality-selector.min.js index e4869564..88621e8d 100644 --- a/assets/js/silvermine-videojs-quality-selector.min.js +++ b/assets/js/silvermine-videojs-quality-selector.min.js @@ -1,3 +1,4 @@ -/*! @silvermine/videojs-quality-selector 2019-09-26 v1.2.2-4-gc134430-dirty */ +/*! @silvermine/videojs-quality-selector 2020-03-02 v1.1.2-36-g64d620a-dirty */ -!function u(o,c,a){function l(e,n){if(!c[e]){if(!o[e]){var t="function"==typeof require&&require;if(!n&&t)return t(e,!0);if(s)return s(e,!0);var r=new Error("Cannot find module '"+e+"'");throw r.code="MODULE_NOT_FOUND",r}var i=c[e]={exports:{}};o[e][0].call(i.exports,function(n){return l(o[e][1][n]||n)},i,i.exports,u,o,c,a)}return c[e].exports}for(var s="function"==typeof require&&require,n=0;n":">",'"':""","'":"'","`":"`"},B=h.invert(D);h.escape=W(D),h.unescape=W(B),h.result=function(n,e,t){h.isArray(e)||(e=[e]);var r=e.length;if(!r)return h.isFunction(t)?t.call(n):t;for(var i=0;i/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};function z(n){return"\\"+K[n]}var Y=/(.)^/,K={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},V=/\\|'|\r|\n|\u2028|\u2029/g;h.template=function(u,n,e){!n&&e&&(n=e),n=h.defaults({},n,h.templateSettings);var t,r=RegExp([(n.escape||Y).source,(n.interpolate||Y).source,(n.evaluate||Y).source].join("|")+"|$","g"),o=0,c="__p+='";u.replace(r,function(n,e,t,r,i){return c+=u.slice(o,i).replace(V,z),o=i+n.length,e?c+="'+\n((__t=("+e+"))==null?'':_.escape(__t))+\n'":t?c+="'+\n((__t=("+t+"))==null?'':__t)+\n'":r&&(c+="';\n"+r+"\n__p+='"),n}),c+="';\n",n.variable||(c="with(obj||{}){\n"+c+"}\n"),c="var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');};\n"+c+"return __p;\n";try{t=new Function(n.variable||"obj","_",c)}catch(n){throw n.source=c,n}function i(n){return t.call(this,n,h)}var a=n.variable||"obj";return i.source="function("+a+"){\n"+c+"}",i},h.chain=function(n){var e=h(n);return e._chain=!0,e};function J(n,e){return n._chain?h(e).chain():e}h.mixin=function(t){return h.each(h.functions(t),function(n){var e=h[n]=t[n];h.prototype[n]=function(){var n=[this._wrapped];return i.apply(n,arguments),J(this,e.apply(h,n))}}),h},h.mixin(h),h.each(["pop","push","reverse","shift","sort","splice","unshift"],function(e){var t=r[e];h.prototype[e]=function(){var n=this._wrapped;return t.apply(n,arguments),"shift"!==e&&"splice"!==e||0!==n.length||delete n[0],J(this,n)}}),h.each(["concat","join","slice"],function(n){var e=r[n];h.prototype[n]=function(){return J(this,e.apply(this._wrapped,arguments))}}),h.prototype.value=function(){return this._wrapped},h.prototype.valueOf=h.prototype.toJSON=h.prototype.value,h.prototype.toString=function(){return String(this._wrapped)},"function"==typeof define&&define.amd&&define("underscore",[],function(){return h})}()}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],3:[function(n,e,t){"use strict";var i=n("underscore"),u=n("../events");e.exports=function(n){var r=n.getComponent("MenuItem");return n.extend(r,{constructor:function(n,e){var t=e.source;if(!i.isObject(t))throw new Error('was not provided a "source" object, but rather: '+typeof t);e=i.extend({selectable:!0,label:t.label},e),r.call(this,n,e),this.source=t},handleClick:function(n){r.prototype.handleClick.call(this,n),this.player().trigger(u.QUALITY_REQUESTED,this.source)}})}},{"../events":5,underscore:2}],4:[function(n,e,t){"use strict";var i=n("underscore"),u=n("../events"),o=n("./QualityOption"),c="vjs-quality-changing";e.exports=function(n){var e,r=n.getComponent("MenuButton"),t=o(n);return e=n.extend(r,{constructor:function(t,n){r.call(this,t,n),t.on(u.QUALITY_REQUESTED,function(n,e){this.setSelectedSource(e),t.addClass(c),t.one("loadeddata",function(){t.removeClass(c)})}.bind(this)),t.on(u.QUALITY_SELECTED,function(n,e){this.setSelectedSource(e)}.bind(this)),t.one("ready",function(){this.selectedSrc=t.src(),this.update()}.bind(this)),this.controlText("Open quality selector menu")},setSelectedSource:function(n){var e=n?n.src:void 0;this.selectedSrc!==e&&(this.selectedSrc=e,i.each(this.items,function(n){n.selected(n.source.src===e)}))},createItems:function(){var e=this.player(),n=e.currentSources();return i.map(n,function(n){return new t(e,{source:n,selected:n.src===this.selectedSrc})}.bind(this))},buildWrapperCSSClass:function(){return"vjs-quality-selector "+r.prototype.buildWrapperCSSClass.call(this)}}),n.registerComponent("QualitySelector",e),e}},{"../events":5,"./QualityOption":3,underscore:2}],5:[function(n,e,t){"use strict";e.exports={QUALITY_REQUESTED:"qualityRequested",QUALITY_SELECTED:"qualitySelected"}},{}],6:[function(n,e,t){"use strict";var c=n("underscore"),r=n("./events"),i=n("./components/QualitySelector"),u=n("./middleware/SourceInterceptor"),a=n("./util/SafeSeek");e.exports=function(n){n=n||window.videojs,i(n),u(n),n.hook("setup",function(o){o.on(r.QUALITY_REQUESTED,function(n,e){var t=o.currentSources(),r=o.currentTime(),i=o.playbackRate(),u=o.paused();c.each(t,function(n){n.selected=!1}),c.findWhere(t,{src:e.src}).selected=!0,o._qualitySelectorSafeSeek&&o._qualitySelectorSafeSeek.onQualitySelectionChange(),o.src(t),o.ready(function(){o._qualitySelectorSafeSeek&&!o._qualitySelectorSafeSeek.hasFinished()||(o._qualitySelectorSafeSeek=new a(o,r),o.playbackRate(i)),u||o.play()})})})},e.exports.EVENTS=r},{"./components/QualitySelector":4,"./events":5,"./middleware/SourceInterceptor":7,"./util/SafeSeek":9,underscore:2}],7:[function(n,e,t){"use strict";var u=n("underscore"),o=n("../events");e.exports=function(n){n.use("*",function(i){return{setSource:function(n,e){var t,r=i.currentSources();i._qualitySelectorSafeSeek&&i._qualitySelectorSafeSeek.onPlayerSourcesChange(),t=u.find(r,function(n){return!0===n.selected||"true"===n.selected})||n,i.trigger(o.QUALITY_SELECTED,t),e(null,t)}}})}},{"../events":5,underscore:2}],8:[function(n,e,t){"use strict";n("./index")()},{"./index":6}],9:[function(n,e,t){"use strict";var r=n("class.extend");e.exports=r.extend({init:function(n,e){this._player=n,this._seekToTime=e,this._hasFinished=!1,this._keepThisInstanceWhenPlayerSourcesChange=!1,this._seekWhenSafe()},_seekWhenSafe:function(){this._player.readyState()<3?(this._seekFn=this._seek.bind(this),this._player.one("canplay",this._seekFn)):this._seek()},onPlayerSourcesChange:function(){this._keepThisInstanceWhenPlayerSourcesChange?this._keepThisInstanceWhenPlayerSourcesChange=!1:this.cancel()},onQualitySelectionChange:function(){this.hasFinished()||(this._keepThisInstanceWhenPlayerSourcesChange=!0)},_seek:function(){this._player.currentTime(this._seekToTime),this._keepThisInstanceWhenPlayerSourcesChange=!1,this._hasFinished=!0},hasFinished:function(){return this._hasFinished},cancel:function(){this._player.off("canplay",this._seekFn),this._keepThisInstanceWhenPlayerSourcesChange=!1,this._hasFinished=!0}})},{"class.extend":1}]},{},[8]); \ No newline at end of file +!function u(o,c,a){function l(e,n){if(!c[e]){if(!o[e]){var t="function"==typeof require&&require;if(!n&&t)return t(e,!0);if(s)return s(e,!0);var r=new Error("Cannot find module '"+e+"'");throw r.code="MODULE_NOT_FOUND",r}var i=c[e]={exports:{}};o[e][0].call(i.exports,function(n){return l(o[e][1][n]||n)},i,i.exports,u,o,c,a)}return c[e].exports}for(var s="function"==typeof require&&require,n=0;n":">",'"':""","'":"'","`":"`"},W=h.invert(P);h.escape=D(P),h.unescape=D(W),h.result=function(n,e,t){h.isArray(e)||(e=[e]);var r=e.length;if(!r)return h.isFunction(t)?t.call(n):t;for(var i=0;i/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};function Y(n){return"\\"+K[n]}var z=/(.)^/,K={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},G=/\\|'|\r|\n|\u2028|\u2029/g;h.template=function(u,n,e){!n&&e&&(n=e),n=h.defaults({},n,h.templateSettings);var t,r=RegExp([(n.escape||z).source,(n.interpolate||z).source,(n.evaluate||z).source].join("|")+"|$","g"),o=0,c="__p+='";u.replace(r,function(n,e,t,r,i){return c+=u.slice(o,i).replace(G,Y),o=i+n.length,e?c+="'+\n((__t=("+e+"))==null?'':_.escape(__t))+\n'":t?c+="'+\n((__t=("+t+"))==null?'':__t)+\n'":r&&(c+="';\n"+r+"\n__p+='"),n}),c+="';\n",n.variable||(c="with(obj||{}){\n"+c+"}\n"),c="var __t,__p='',__j=Array.prototype.join,print=function(){__p+=__j.call(arguments,'');};\n"+c+"return __p;\n";try{t=new Function(n.variable||"obj","_",c)}catch(n){throw n.source=c,n}function i(n){return t.call(this,n,h)}var a=n.variable||"obj";return i.source="function("+a+"){\n"+c+"}",i},h.chain=function(n){var e=h(n);return e._chain=!0,e};function H(n,e){return n._chain?h(e).chain():e}h.mixin=function(t){return h.each(h.functions(t),function(n){var e=h[n]=t[n];h.prototype[n]=function(){var n=[this._wrapped];return i.apply(n,arguments),H(this,e.apply(h,n))}}),h},h.mixin(h),h.each(["pop","push","reverse","shift","sort","splice","unshift"],function(e){var t=r[e];h.prototype[e]=function(){var n=this._wrapped;return t.apply(n,arguments),"shift"!==e&&"splice"!==e||0!==n.length||delete n[0],H(this,n)}}),h.each(["concat","join","slice"],function(n){var e=r[n];h.prototype[n]=function(){return H(this,e.apply(this._wrapped,arguments))}}),h.prototype.value=function(){return this._wrapped},h.prototype.valueOf=h.prototype.toJSON=h.prototype.value,h.prototype.toString=function(){return String(this._wrapped)},"function"==typeof define&&define.amd&&define("underscore",[],function(){return h})}()}).call(this,"undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{}],3:[function(n,e,t){"use strict";var i=n("underscore"),u=n("../events");e.exports=function(n){var r=n.getComponent("MenuItem");return n.extend(r,{constructor:function(n,e){var t=e.source;if(!i.isObject(t))throw new Error('was not provided a "source" object, but rather: '+typeof t);e=i.extend({selectable:!0,label:t.label},e),r.call(this,n,e),this.source=t},handleClick:function(n){r.prototype.handleClick.call(this,n),this.player().trigger(u.QUALITY_REQUESTED,this.source)}})}},{"../events":5,underscore:2}],4:[function(n,e,t){"use strict";var i=n("underscore"),u=n("../events"),o=n("./QualityOption"),c="vjs-quality-changing";e.exports=function(n){var e,r=n.getComponent("MenuButton"),t=o(n);return e=n.extend(r,{constructor:function(t,n){r.call(this,t,n),t.on(u.QUALITY_REQUESTED,function(n,e){this.setSelectedSource(e),t.addClass(c),t.one("loadeddata",function(){t.removeClass(c)})}.bind(this)),t.on(u.PLAYER_SOURCES_CHANGED,function(){this.update()}.bind(this)),t.on(u.QUALITY_SELECTED,function(n,e){this.setSelectedSource(e)}.bind(this)),t.one("ready",function(){this.selectedSrc=t.src(),this.update()}.bind(this)),this.controlText("Open quality selector menu")},setSelectedSource:function(n){var e=n?n.src:void 0;this.selectedSrc!==e&&(this.selectedSrc=e,i.each(this.items,function(n){n.selected(n.source.src===e)}))},createItems:function(){var e=this.player(),n=e.currentSources();return i.map(n,function(n){return new t(e,{source:n,selected:n.src===this.selectedSrc})}.bind(this))},buildWrapperCSSClass:function(){return"vjs-quality-selector "+r.prototype.buildWrapperCSSClass.call(this)}}),n.registerComponent("QualitySelector",e),e}},{"../events":5,"./QualityOption":3,underscore:2}],5:[function(n,e,t){"use strict";e.exports={QUALITY_REQUESTED:"qualityRequested",QUALITY_SELECTED:"qualitySelected",PLAYER_SOURCES_CHANGED:"playerSourcesChanged"}},{}],6:[function(n,e,t){"use strict";var c=n("underscore"),r=n("./events"),i=n("./components/QualitySelector"),u=n("./middleware/SourceInterceptor"),a=n("./util/SafeSeek");e.exports=function(n){n=n||window.videojs,i(n),u(n),n.hook("setup",function(o){o.on(r.QUALITY_REQUESTED,function(n,e){var t=o.currentSources(),r=o.currentTime(),i=o.playbackRate(),u=o.paused();c.each(t,function(n){n.selected=!1}),c.findWhere(t,{src:e.src}).selected=!0,o._qualitySelectorSafeSeek&&o._qualitySelectorSafeSeek.onQualitySelectionChange(),o.src(t),o.ready(function(){o._qualitySelectorSafeSeek&&!o._qualitySelectorSafeSeek.hasFinished()||(o._qualitySelectorSafeSeek=new a(o,r),o.playbackRate(i)),u||o.play()})})})},e.exports.EVENTS=r},{"./components/QualitySelector":4,"./events":5,"./middleware/SourceInterceptor":7,"./util/SafeSeek":9,underscore:2}],7:[function(n,e,t){"use strict";var u=n("underscore"),o=n("../events");e.exports=function(n){n.use("*",function(i){return{setSource:function(n,e){var t,r=i.currentSources();i._qualitySelectorSafeSeek&&i._qualitySelectorSafeSeek.onPlayerSourcesChange(),u.isEqual(r,i._qualitySelectorPreviousSources)||(i.trigger(o.PLAYER_SOURCES_CHANGED,r),i._qualitySelectorPreviousSources=r),t=u.find(r,function(n){return!0===n.selected||"true"===n.selected||"selected"===n.selected})||n,i.trigger(o.QUALITY_SELECTED,t),e(null,t)}}})}},{"../events":5,underscore:2}],8:[function(n,e,t){"use strict";n("./index")()},{"./index":6}],9:[function(n,e,t){"use strict";var r=n("class.extend");e.exports=r.extend({init:function(n,e){this._player=n,this._seekToTime=e,this._hasFinished=!1,this._keepThisInstanceWhenPlayerSourcesChange=!1,this._seekWhenSafe()},_seekWhenSafe:function(){this._player.readyState()<3?(this._seekFn=this._seek.bind(this),this._player.one("canplay",this._seekFn)):this._seek()},onPlayerSourcesChange:function(){this._keepThisInstanceWhenPlayerSourcesChange?this._keepThisInstanceWhenPlayerSourcesChange=!1:this.cancel()},onQualitySelectionChange:function(){this.hasFinished()||(this._keepThisInstanceWhenPlayerSourcesChange=!0)},_seek:function(){this._player.currentTime(this._seekToTime),this._keepThisInstanceWhenPlayerSourcesChange=!1,this._hasFinished=!0},hasFinished:function(){return this._hasFinished},cancel:function(){this._player.off("canplay",this._seekFn),this._keepThisInstanceWhenPlayerSourcesChange=!1,this._hasFinished=!0}})},{"class.extend":1}]},{},[8]); +//# sourceMappingURL=silvermine-videojs-quality-selector.min.js.map \ No newline at end of file diff --git a/assets/js/subscribe_widget.js b/assets/js/subscribe_widget.js index 6c21bffb..216c36fe 100644 --- a/assets/js/subscribe_widget.js +++ b/assets/js/subscribe_widget.js @@ -1,3 +1,5 @@ +var subscribe_data = JSON.parse(document.getElementById('subscribe_data').innerHTML); + var subscribe_button = document.getElementById('subscribe'); subscribe_button.parentNode['action'] = 'javascript:void(0)'; diff --git a/assets/js/watch.js b/assets/js/watch.js index a26cb505..e9ad2ddc 100644 --- a/assets/js/watch.js +++ b/assets/js/watch.js @@ -1,3 +1,5 @@ +var video_data = JSON.parse(document.getElementById('video_data').innerHTML); + String.prototype.supplant = function (o) { return this.replace(/{([^{}]*)}/g, function (a, b) { var r = o[b]; diff --git a/assets/js/watched_widget.js b/assets/js/watched_widget.js index 1e88fa27..ba741974 100644 --- a/assets/js/watched_widget.js +++ b/assets/js/watched_widget.js @@ -1,3 +1,5 @@ +var watched_data = JSON.parse(document.getElementById('watched_data').innerHTML); + function mark_watched(target) { var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode; tile.style.display = 'none'; diff --git a/src/invidious.cr b/src/invidious.cr index e89c2c37..800af0dd 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -248,10 +248,20 @@ spawn do end before_all do |env| - host_url = make_host_url(config, Kemal.config) + begin + preferences = Preferences.from_json(env.request.cookies["PREFS"]?.try &.value || "{}") + rescue + preferences = Preferences.from_json("{}") + end + env.response.headers["X-XSS-Protection"] = "1; mode=block" env.response.headers["X-Content-Type-Options"] = "nosniff" - env.response.headers["Content-Security-Policy"] = "default-src blob: data: 'self' #{host_url} 'unsafe-inline' 'unsafe-eval'; media-src blob: 'self' #{host_url} https://*.googlevideo.com:443" + extra_media_csp = "" + if CONFIG.disabled?("local") || !preferences.local + extra_media_csp += " https://*.googlevideo.com:443" + end + # TODO: Remove style-src's 'unsafe-inline', requires to remove all inline styles (, style=" [..] ") + env.response.headers["Content-Security-Policy"] = "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; media-src 'self' blob:#{extra_media_csp}" env.response.headers["Referrer-Policy"] = "same-origin" if (Kemal.config.ssl || config.https_only) && config.hsts @@ -269,12 +279,6 @@ before_all do |env| "/latest_version", }.any? { |r| env.request.resource.starts_with? r } - begin - preferences = Preferences.from_json(env.request.cookies["PREFS"]?.try &.value || "{}") - rescue - preferences = Preferences.from_json("{}") - end - if env.request.cookies.has_key? "SID" sid = env.request.cookies["SID"].value diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr index 2938247a..4a048d7a 100644 --- a/src/invidious/comments.cr +++ b/src/invidious/comments.cr @@ -294,7 +294,7 @@ def template_youtube_comments(comments, locale, thin_mode)

#{translate(locale, "View `x` replies", number_with_separator(child["replies"]["replyCount"]))} + data-onclick="get_youtube_replies">#{translate(locale, "View `x` replies", number_with_separator(child["replies"]["replyCount"]))}

@@ -413,7 +413,7 @@ def template_youtube_comments(comments, locale, thin_mode)

#{translate(locale, "Load more")} + data-onclick="get_youtube_replies" data-load-more>#{translate(locale, "Load more")}

@@ -451,7 +451,7 @@ def template_reddit_comments(root, locale) html << <<-END_HTML

- [ - ] + [ - ] #{child.author} #{translate(locale, "`x` points", number_with_separator(child.score))} #{translate(locale, "`x` ago", recode_date(child.created_utc, locale))} @@ -556,7 +556,7 @@ def content_to_comment_html(content) video_id = watch_endpoint["videoId"].as_s if length_seconds - text = %(#{text}) + text = %(#{text}) else text = %(#{text}) end diff --git a/src/invidious/views/add_playlist_items.ecr b/src/invidious/views/add_playlist_items.ecr index f1899faa..dff0b549 100644 --- a/src/invidious/views/add_playlist_items.ecr +++ b/src/invidious/views/add_playlist_items.ecr @@ -20,9 +20,9 @@

- diff --git a/src/invidious/views/community.ecr b/src/invidious/views/community.ecr index 218cc2d4..fca8c0b6 100644 --- a/src/invidious/views/community.ecr +++ b/src/invidious/views/community.ecr @@ -71,14 +71,14 @@
<% end %> - diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr index 9669aaeb..e9baba2c 100644 --- a/src/invidious/views/components/item.ecr +++ b/src/invidious/views/components/item.ecr @@ -57,10 +57,10 @@
<% if plid = env.get?("remove_playlist_items") %> -
" method="post"> + " method="post"> ">

- + @@ -103,13 +103,12 @@

<% if env.get? "show_watched" %> - " method="post"> + " method="post"> ">

- + @@ -117,10 +116,10 @@

<% elsif plid = env.get? "add_playlist_items" %> -
" method="post"> + " method="post"> ">

- + diff --git a/src/invidious/views/components/player.ecr b/src/invidious/views/components/player.ecr index ba6311cb..520d72dd 100644 --- a/src/invidious/views/components/player.ecr +++ b/src/invidious/views/components/player.ecr @@ -1,8 +1,5 @@ - diff --git a/src/invidious/views/components/player_sources.ecr b/src/invidious/views/components/player_sources.ecr index d950e0da..8162546e 100644 --- a/src/invidious/views/components/player_sources.ecr +++ b/src/invidious/views/components/player_sources.ecr @@ -3,6 +3,7 @@ + diff --git a/src/invidious/views/components/subscribe_widget.ecr b/src/invidious/views/components/subscribe_widget.ecr index 471e6c1c..7c579a8a 100644 --- a/src/invidious/views/components/subscribe_widget.ecr +++ b/src/invidious/views/components/subscribe_widget.ecr @@ -19,14 +19,14 @@

<% end %> - diff --git a/src/invidious/views/embed.ecr b/src/invidious/views/embed.ecr index 6c06bf2e..ff8277e8 100644 --- a/src/invidious/views/embed.ecr +++ b/src/invidious/views/embed.ecr @@ -10,32 +10,21 @@ + <%= HTML.escape(video.title) %> - Invidious - - diff --git a/src/invidious/views/history.ecr b/src/invidious/views/history.ecr index 7d7ded2c..2aa8adf7 100644 --- a/src/invidious/views/history.ecr +++ b/src/invidious/views/history.ecr @@ -18,9 +18,9 @@
- @@ -34,10 +34,10 @@ var watched_data = { <% if !env.get("preferences").as(Preferences).thin_mode %> <% if playlist.is_a?(InvidiousPlaylist) && playlist.author == user.try &.email %> - diff --git a/src/invidious/views/preferences.ecr b/src/invidious/views/preferences.ecr index 17e5804e..7e899133 100644 --- a/src/invidious/views/preferences.ecr +++ b/src/invidious/views/preferences.ecr @@ -2,12 +2,6 @@ <%= translate(locale, "Preferences") %> - Invidious <% end %> - -
@@ -65,7 +59,7 @@ function update_value(element) {
- + <%= preferences.volume %>
@@ -205,7 +199,7 @@ function update_value(element) { <% # Web notifications are only supported over HTTPS %> <% if Kemal.config.ssl || config.https_only %>
<% end %> <% end %> diff --git a/src/invidious/views/subscription_manager.ecr b/src/invidious/views/subscription_manager.ecr index 43d14b37..6cddcd6c 100644 --- a/src/invidious/views/subscription_manager.ecr +++ b/src/invidious/views/subscription_manager.ecr @@ -37,9 +37,9 @@

- " method="post"> + " method="post"> "> - + "> @@ -52,32 +52,3 @@ <% end %>

<% end %> - - diff --git a/src/invidious/views/subscriptions.ecr b/src/invidious/views/subscriptions.ecr index ee31d241..93c58471 100644 --- a/src/invidious/views/subscriptions.ecr +++ b/src/invidious/views/subscriptions.ecr @@ -45,9 +45,9 @@
- diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr index d2ef9c7e..b239d22a 100644 --- a/src/invidious/views/template.ecr +++ b/src/invidious/views/template.ecr @@ -147,13 +147,14 @@
+ <% if env.get? "user" %> - diff --git a/src/invidious/views/token_manager.ecr b/src/invidious/views/token_manager.ecr index b626d99c..e48aec2f 100644 --- a/src/invidious/views/token_manager.ecr +++ b/src/invidious/views/token_manager.ecr @@ -29,9 +29,9 @@

-
" method="post"> + " method="post"> "> - + ">
@@ -44,32 +44,3 @@ <% end %>

<% end %> - - diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index df61abc5..16ac71eb 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -26,23 +26,23 @@ <%= HTML.escape(video.title) %> - Invidious <% end %> - From 4011a113ccc1241b60f607ce76db982625f7b9b1 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Sun, 15 Mar 2020 17:37:51 -0400 Subject: [PATCH 0076/2930] Strip invalid characters from referer URLs --- src/invidious/helpers/utils.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index 7c5edc5c..a0a619fa 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -316,7 +316,7 @@ def get_referer(env, fallback = "/", unroll = true) end referer = referer.full_path - referer = "/" + referer.lstrip("/\\") + referer = "/" + referer.gsub(/[^\/?@&%=\-_.0-9a-zA-Z]/, "").lstrip("/\\") if referer == env.request.path referer = fallback From 59a15ceef6e5f02a0e16e6604bc61ce41227d413 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Sun, 15 Mar 2020 17:39:21 -0400 Subject: [PATCH 0077/2930] Remove VarInt class --- src/invidious/helpers/utils.cr | 37 ---------------------------------- 1 file changed, 37 deletions(-) diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index a0a619fa..d0892862 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -325,43 +325,6 @@ def get_referer(env, fallback = "/", unroll = true) return referer end -struct VarInt - def self.from_io(io : IO, format = IO::ByteFormat::NetworkEndian) : Int32 - result = 0_u32 - num_read = 0 - - loop do - byte = io.read_byte - raise "Invalid VarInt" if !byte - value = byte & 0x7f - - result |= value.to_u32 << (7 * num_read) - num_read += 1 - - break if byte & 0x80 == 0 - raise "Invalid VarInt" if num_read > 5 - end - - result.to_i32 - end - - def self.to_io(io : IO, value : Int32) - io.write_byte 0x00 if value == 0x00 - value = value.to_u32 - - while value != 0 - byte = (value & 0x7f).to_u8 - value >>= 7 - - if value != 0 - byte |= 0x80 - end - - io.write_byte byte - end - end -end - def sha256(text) digest = OpenSSL::Digest.new("SHA256") digest << text From bd7950b7579426d3acdf881262e802678e2c336d Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Sun, 15 Mar 2020 18:52:49 -0400 Subject: [PATCH 0078/2930] Add toggle_parent to dynamic handlers --- assets/js/handlers.js | 86 +++++++++++++++++++++---------------------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/assets/js/handlers.js b/assets/js/handlers.js index 68ba9f4f..77062ca6 100644 --- a/assets/js/handlers.js +++ b/assets/js/handlers.js @@ -1,78 +1,78 @@ 'use strict'; -(function() { - var n2a = function(n) { return Array.prototype.slice.call(n); }; +(function () { + var n2a = function (n) { return Array.prototype.slice.call(n); }; var video_player = document.getElementById('player'); if (video_player) { - video_player.onmouseenter = function() { video_player['data-title'] = video_player['title']; video_player['title'] = ''; }; - video_player.onmouseleave = function() { video_player['title'] = video_player['data-title']; video_player['data-title'] = ''; }; - video_player.oncontextmenu = function() { video_player['title'] = video_player['data-title']; }; + video_player.onmouseenter = function () { video_player['data-title'] = video_player['title']; video_player['title'] = ''; }; + video_player.onmouseleave = function () { video_player['title'] = video_player['data-title']; video_player['data-title'] = ''; }; + video_player.oncontextmenu = function () { video_player['title'] = video_player['data-title']; }; } // For dynamically inserted elements - document.addEventListener('click', function(e) { + document.addEventListener('click', function (e) { if (!e || !e.target) { return; } e = e.target; var handler_name = e.getAttribute('data-onclick'); switch (handler_name) { - case 'jump_to_time': - var time = e.getAttribute('data-jump-time'); - player.currentTime(time); - break; - case 'get_youtube_replies': - var load_more = e.getAttribute('data-load-more') !== null; - get_youtube_replies(e, load_more); - break; - default: - break; + case 'jump_to_time': + var time = e.getAttribute('data-jump-time'); + player.currentTime(time); + break; + case 'get_youtube_replies': + var load_more = e.getAttribute('data-load-more') !== null; + get_youtube_replies(e, load_more); + break; + case 'toggle_parent': + toggle_parent(e); + break; + default: + break; } }); - n2a(document.querySelectorAll('[data-mouse="switch_classes"]')).forEach(function(e) { + n2a(document.querySelectorAll('[data-mouse="switch_classes"]')).forEach(function (e) { var classes = e.getAttribute('data-switch-classes').split(','); var ec = classes[0]; var lc = classes[1]; - var onoff = function(on, off) { + var onoff = function (on, off) { var cs = e.getAttribute('class'); cs = cs.split(off).join(on); e.setAttribute('class', cs); }; - e.onmouseenter = function() { onoff(ec, lc); }; - e.onmouseleave = function() { onoff(lc, ec); }; + e.onmouseenter = function () { onoff(ec, lc); }; + e.onmouseleave = function () { onoff(lc, ec); }; }); - n2a(document.querySelectorAll('[data-onsubmit="return_false"]')).forEach(function(e) { - e.onsubmit = function() { return false; }; + n2a(document.querySelectorAll('[data-onsubmit="return_false"]')).forEach(function (e) { + e.onsubmit = function () { return false; }; }); - n2a(document.querySelectorAll('[data-onclick="toggle_parent"]')).forEach(function(e) { - e.onclick = function() { toggle_parent(e); }; + n2a(document.querySelectorAll('[data-onclick="mark_watched"]')).forEach(function (e) { + e.onclick = function () { mark_watched(e); }; }); - n2a(document.querySelectorAll('[data-onclick="mark_watched"]')).forEach(function(e) { - e.onclick = function() { mark_watched(e); }; + n2a(document.querySelectorAll('[data-onclick="mark_unwatched"]')).forEach(function (e) { + e.onclick = function () { mark_unwatched(e); }; }); - n2a(document.querySelectorAll('[data-onclick="mark_unwatched"]')).forEach(function(e) { - e.onclick = function() { mark_unwatched(e); }; + n2a(document.querySelectorAll('[data-onclick="add_playlist_item"]')).forEach(function (e) { + e.onclick = function () { add_playlist_item(e); }; }); - n2a(document.querySelectorAll('[data-onclick="add_playlist_item"]')).forEach(function(e) { - e.onclick = function() { add_playlist_item(e); }; + n2a(document.querySelectorAll('[data-onclick="remove_playlist_item"]')).forEach(function (e) { + e.onclick = function () { remove_playlist_item(e); }; }); - n2a(document.querySelectorAll('[data-onclick="remove_playlist_item"]')).forEach(function(e) { - e.onclick = function() { remove_playlist_item(e); }; + n2a(document.querySelectorAll('[data-onclick="revoke_token"]')).forEach(function (e) { + e.onclick = function () { revoke_token(e); }; }); - n2a(document.querySelectorAll('[data-onclick="revoke_token"]')).forEach(function(e) { - e.onclick = function() { revoke_token(e); }; + n2a(document.querySelectorAll('[data-onclick="remove_subscription"]')).forEach(function (e) { + e.onclick = function () { remove_subscription(e); }; }); - n2a(document.querySelectorAll('[data-onclick="remove_subscription"]')).forEach(function(e) { - e.onclick = function() { remove_subscription(e); }; - }); - n2a(document.querySelectorAll('[data-onclick="notification_requestPermission"]')).forEach(function(e) { - e.onclick = function() { Notification.requestPermission(); }; + n2a(document.querySelectorAll('[data-onclick="notification_requestPermission"]')).forEach(function (e) { + e.onclick = function () { Notification.requestPermission(); }; }); - n2a(document.querySelectorAll('[data-onrange="update_volume_value"]')).forEach(function(e) { - var cb = function() { update_volume_value(e); } + n2a(document.querySelectorAll('[data-onrange="update_volume_value"]')).forEach(function (e) { + var cb = function () { update_volume_value(e); } e.oninput = cb; e.onchange = cb; }); @@ -97,7 +97,7 @@ xhr.open('POST', url, true); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); - xhr.onreadystatechange = function() { + xhr.onreadystatechange = function () { if (xhr.readyState == 4) { if (xhr.status != 200) { count.innerText = parseInt(count.innerText) + 1; @@ -126,7 +126,7 @@ xhr.open('POST', url, true); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); - xhr.onreadystatechange = function() { + xhr.onreadystatechange = function () { if (xhr.readyState == 4) { if (xhr.status != 200) { count.innerText = parseInt(count.innerText) + 1; From 92798abb5d2731d6336da907113f2af407944f6d Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Thu, 19 Mar 2020 13:37:22 -0500 Subject: [PATCH 0079/2930] Add manifest-src to CSP --- src/invidious.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious.cr b/src/invidious.cr index 800af0dd..73546d7d 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -261,7 +261,7 @@ before_all do |env| extra_media_csp += " https://*.googlevideo.com:443" end # TODO: Remove style-src's 'unsafe-inline', requires to remove all inline styles (, style=" [..] ") - env.response.headers["Content-Security-Policy"] = "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; media-src 'self' blob:#{extra_media_csp}" + env.response.headers["Content-Security-Policy"] = "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; manifest-src 'self'; media-src 'self' blob:#{extra_media_csp}" env.response.headers["Referrer-Policy"] = "same-origin" if (Kemal.config.ssl || config.https_only) && config.hsts From 0e58d99f4e17618d67fb78d79b10a11fb0b0811d Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Fri, 27 Mar 2020 09:47:46 -0500 Subject: [PATCH 0080/2930] Fix player mouseover events --- assets/js/handlers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/handlers.js b/assets/js/handlers.js index 77062ca6..7ecb5a02 100644 --- a/assets/js/handlers.js +++ b/assets/js/handlers.js @@ -3,7 +3,7 @@ (function () { var n2a = function (n) { return Array.prototype.slice.call(n); }; - var video_player = document.getElementById('player'); + var video_player = document.getElementById('player_html5_api'); if (video_player) { video_player.onmouseenter = function () { video_player['data-title'] = video_player['title']; video_player['title'] = ''; }; video_player.onmouseleave = function () { video_player['title'] = video_player['data-title']; video_player['data-title'] = ''; }; From 3b2e142542cf051c89b7a6b678463b0693489b91 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Sun, 29 Mar 2020 17:44:45 -0400 Subject: [PATCH 0081/2930] Fix JSON serialization --- src/invidious/views/add_playlist_items.ecr | 6 ++-- src/invidious/views/community.ecr | 16 +++++---- src/invidious/views/components/player.ecr | 12 ++++--- .../views/components/subscribe_widget.ecr | 16 +++++---- src/invidious/views/embed.ecr | 20 ++++++----- src/invidious/views/history.ecr | 6 ++-- src/invidious/views/playlist.ecr | 6 ++-- src/invidious/views/subscriptions.ecr | 6 ++-- src/invidious/views/template.ecr | 10 +++--- src/invidious/views/watch.ecr | 34 ++++++++++--------- 10 files changed, 76 insertions(+), 56 deletions(-) diff --git a/src/invidious/views/add_playlist_items.ecr b/src/invidious/views/add_playlist_items.ecr index dff0b549..07295c1a 100644 --- a/src/invidious/views/add_playlist_items.ecr +++ b/src/invidious/views/add_playlist_items.ecr @@ -21,9 +21,11 @@ diff --git a/src/invidious/views/community.ecr b/src/invidious/views/community.ecr index fca8c0b6..69724390 100644 --- a/src/invidious/views/community.ecr +++ b/src/invidious/views/community.ecr @@ -72,13 +72,15 @@ <% end %> diff --git a/src/invidious/views/components/player.ecr b/src/invidious/views/components/player.ecr index 520d72dd..3c30f69e 100644 --- a/src/invidious/views/components/player.ecr +++ b/src/invidious/views/components/player.ecr @@ -37,11 +37,13 @@ diff --git a/src/invidious/views/components/subscribe_widget.ecr b/src/invidious/views/components/subscribe_widget.ecr index 7c579a8a..ac2fbf1d 100644 --- a/src/invidious/views/components/subscribe_widget.ecr +++ b/src/invidious/views/components/subscribe_widget.ecr @@ -20,14 +20,16 @@ <% end %> <% else %> diff --git a/src/invidious/views/embed.ecr b/src/invidious/views/embed.ecr index ff8277e8..48dbc55f 100644 --- a/src/invidious/views/embed.ecr +++ b/src/invidious/views/embed.ecr @@ -16,16 +16,18 @@ <%= rendered "components/player" %> diff --git a/src/invidious/views/history.ecr b/src/invidious/views/history.ecr index 2aa8adf7..fe8c70b9 100644 --- a/src/invidious/views/history.ecr +++ b/src/invidious/views/history.ecr @@ -19,9 +19,11 @@ diff --git a/src/invidious/views/playlist.ecr b/src/invidious/views/playlist.ecr index eff12c48..ccda94d9 100644 --- a/src/invidious/views/playlist.ecr +++ b/src/invidious/views/playlist.ecr @@ -70,9 +70,11 @@ <% if playlist.is_a?(InvidiousPlaylist) && playlist.author == user.try &.email %> <% end %> diff --git a/src/invidious/views/subscriptions.ecr b/src/invidious/views/subscriptions.ecr index 93c58471..af1d4fbc 100644 --- a/src/invidious/views/subscriptions.ecr +++ b/src/invidious/views/subscriptions.ecr @@ -46,9 +46,11 @@ diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr index b239d22a..3a8d47dd 100644 --- a/src/invidious/views/template.ecr +++ b/src/invidious/views/template.ecr @@ -152,10 +152,12 @@ <% if env.get? "user" %> <% end %> diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 16ac71eb..2a99dd5b 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -27,23 +27,25 @@ <% end %>
From 80fc60b5e2d0d2c421b7993495398272c751ec3b Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Mon, 30 Mar 2020 14:23:51 -0500 Subject: [PATCH 0082/2930] Add spec for extract_plid --- spec/helpers_spec.cr | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/spec/helpers_spec.cr b/spec/helpers_spec.cr index 95222e0b..37e36c61 100644 --- a/spec/helpers_spec.cr +++ b/spec/helpers_spec.cr @@ -9,6 +9,7 @@ require "../src/invidious/channels" require "../src/invidious/comments" require "../src/invidious/playlists" require "../src/invidious/search" +require "../src/invidious/trending" require "../src/invidious/users" describe "Helper" do @@ -124,6 +125,15 @@ describe "Helper" do end end + describe "#extract_plid" do + it "correctly extracts playlist ID from trending URL" do + extract_plid("/feed/trending?bp=4gIuCggvbS8wNHJsZhIiUExGZ3F1TG5MNTlhbVBud2pLbmNhZUp3MDYzZlU1M3Q0cA%3D%3D").should eq("PLFgquLnL59amPnwjKncaeJw063fU53t4p") + extract_plid("/feed/trending?bp=4gIvCgkvbS8wYnp2bTISIlBMaUN2Vkp6QnVwS2tDaFNnUDdGWFhDclo2aEp4NmtlTm0%3D").should eq("PLiCvVJzBupKkChSgP7FXXCrZ6hJx6keNm") + extract_plid("/feed/trending?bp=4gIuCggvbS8wNWpoZxIiUEwzWlE1Q3BOdWxRbUtPUDNJekdsYWN0V1c4dklYX0hFUA%3D%3D").should eq("PL3ZQ5CpNulQmKOP3IzGlactWW8vIX_HEP") + extract_plid("/feed/trending?bp=4gIuCggvbS8wMnZ4bhIiUEx6akZiYUZ6c21NUnFhdEJnVTdPeGNGTkZhQ2hqTkVERA%3D%3D").should eq("PLzjFbaFzsmMRqatBgU7OxcFNFaChjNEDD") + end + end + describe "#sign_token" do it "correctly signs a given hash" do token = { From c97cdf551ecd4fb5d467bf260f7b97e05a6d62f8 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Mon, 30 Mar 2020 14:27:07 -0500 Subject: [PATCH 0083/2930] Refactor extract_plid --- src/invidious/trending.cr | 34 +++++++--------------------------- 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/src/invidious/trending.cr b/src/invidious/trending.cr index 3a9c6935..017c42f5 100644 --- a/src/invidious/trending.cr +++ b/src/invidious/trending.cr @@ -39,33 +39,13 @@ def fetch_trending(trending_type, region, locale) end def extract_plid(url) - wrapper = HTTP::Params.parse(URI.parse(url).query.not_nil!)["bp"] - - wrapper = URI.decode_www_form(wrapper) - wrapper = Base64.decode(wrapper) - - # 0xe2 0x02 0x2e - wrapper += 3 - - # 0x0a - wrapper += 1 - - # Looks like "/m/[a-z0-9]{5}", not sure what it does here - - item_size = wrapper[0] - wrapper += 1 - item = wrapper[0, item_size] - wrapper += item.size - - # 0x12 - wrapper += 1 - - plid_size = wrapper[0] - wrapper += 1 - plid = wrapper[0, plid_size] - wrapper += plid.size - - plid = String.new(plid) + plid = URI.parse(url) + .try { |i| HTTP::Params.parse(i.query.not_nil!)["bp"] } + .try { |i| URI.decode_www_form(i) } + .try { |i| Base64.decode(i) } + .try { |i| IO::Memory.new(i) } + .try { |i| Protodec::Any.parse(i) } + .try { |i| i["44:0:embedded"]["2:1:string"].as_s } return plid end From d8fe9a4d29865fcc358f983835b6aabaecb57302 Mon Sep 17 00:00:00 2001 From: Olle Jonsson Date: Thu, 2 Apr 2020 23:16:27 +0200 Subject: [PATCH 0084/2930] nb-NO: Translate "subscription" correctly (#1089) Co-authored-by: Oskar Gewalli --- locales/nb-NO.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/locales/nb-NO.json b/locales/nb-NO.json index 4571d888..cda55a1d 100644 --- a/locales/nb-NO.json +++ b/locales/nb-NO.json @@ -25,13 +25,13 @@ "Import and Export Data": "Importer- og eksporter data", "Import": "Importer", "Import Invidious data": "Importer Invidious-data", - "Import YouTube subscriptions": "Importer YouTube-abonnenter", - "Import FreeTube subscriptions (.db)": "Importer FreeTube-abonnenter (.db)", - "Import NewPipe subscriptions (.json)": "Importer NewPipe-abonnenter (.json)", + "Import YouTube subscriptions": "Importer YouTube-abonnementer", + "Import FreeTube subscriptions (.db)": "Importer FreeTube-abonnementer (.db)", + "Import NewPipe subscriptions (.json)": "Importer NewPipe-abonnementer (.json)", "Import NewPipe data (.zip)": "Importer NewPipe-data (.zip)", "Export": "Eksporter", - "Export subscriptions as OPML": "Eksporter abonnenter som OPML", - "Export subscriptions as OPML (for NewPipe & FreeTube)": "Eksporter abonnenter som OPML (for NewPipe og FreeTube)", + "Export subscriptions as OPML": "Eksporter abonnementer som OPML", + "Export subscriptions as OPML (for NewPipe & FreeTube)": "Eksporter abonnementer som OPML (for NewPipe og FreeTube)", "Export data as JSON": "Eksporter data som JSON", "Delete account?": "Slett konto?", "History": "Historikk", From eb8b0f72cc91c8e1c3ae9a36cc397b2fea8c808c Mon Sep 17 00:00:00 2001 From: Olle Jonsson Date: Thu, 2 Apr 2020 23:26:54 +0200 Subject: [PATCH 0085/2930] Add Swedish translation (#1078) Co-authored-by: Daniel Lublin --- locales/sv-SE.json | 336 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 336 insertions(+) create mode 100644 locales/sv-SE.json diff --git a/locales/sv-SE.json b/locales/sv-SE.json new file mode 100644 index 00000000..719d0692 --- /dev/null +++ b/locales/sv-SE.json @@ -0,0 +1,336 @@ +{ + "`x` subscribers": "`x` prenumeranter", + "`x` videos": "`x` videor", + "`x` playlists": "`x` spellistor", + "LIVE": "LIVE", + "Shared `x` ago": "Delad `x` sedan", + "Unsubscribe": "Avprenumerera", + "Subscribe": "Prenumerera", + "View channel on YouTube": "Visa kanalen på YouTube", + "View playlist on YouTube": "Visa spellistan på YouTube", + "newest": "nyaste", + "oldest": "äldsta", + "popular": "populärt", + "last": "sista", + "Next page": "Nästa sida", + "Previous page": "Tidigare sida", + "Clear watch history?": "Töm visningshistorik?", + "New password": "Nytt lösenord", + "New passwords must match": "Nya lösenord måste stämma överens", + "Cannot change password for Google accounts": "Kan inte ändra lösenord på Google-konton", + "Authorize token?": "Auktorisera åtkomsttoken?", + "Authorize token for `x`?": "Auktorisera åtkomsttoken för `x`?", + "Yes": "Ja", + "No": "Nej", + "Import and Export Data": "Importera och exportera data", + "Import": "Importera", + "Import Invidious data": "Importera Invidious-data", + "Import YouTube subscriptions": "Importera YouTube-prenumerationer", + "Import FreeTube subscriptions (.db)": "Importera FreeTube-prenumerationer (.db)", + "Import NewPipe subscriptions (.json)": "Importera NewPipe-prenumerationer (.json)", + "Import NewPipe data (.zip)": "Importera NewPipe-data (.zip)", + "Export": "Exportera", + "Export subscriptions as OPML": "Exportera prenumerationer som OPML", + "Export subscriptions as OPML (for NewPipe & FreeTube)": "Exportera prenumerationer som OPML (för NewPipe och FreeTube)", + "Export data as JSON": "Exportera data som JSON", + "Delete account?": "Radera konto?", + "History": "Historik", + "An alternative front-end to YouTube": "Ett alternativt gränssnitt till YouTube", + "JavaScript license information": "JavaScript-licensinformation", + "source": "källa", + "Log in": "Logga in", + "Log in/register": "Logga in/registrera", + "Log in with Google": "Logga in med Google", + "User ID": "Användar-ID", + "Password": "Lösenord", + "Time (h:mm:ss):": "Tid (h:mm:ss):", + "Text CAPTCHA": "Text-CAPTCHA", + "Image CAPTCHA": "Bild-CAPTCHA", + "Sign In": "Inloggning", + "Register": "Registrera", + "E-mail": "E-post", + "Google verification code": "Google-bekräftelsekod", + "Preferences": "Inställningar", + "Player preferences": "Spelarinställningar", + "Always loop: ": "Loopa alltid: ", + "Autoplay: ": "Autouppspelning: ", + "Play next by default: ": "Spela nästa som förval: ", + "Autoplay next video: ": "Autouppspela nästa video: ", + "Listen by default: ": "Lyssna som förval: ", + "Proxy videos: ": "Proxy:a videor: ", + "Default speed: ": "Förvald hastighet: ", + "Preferred video quality: ": "Föredragen videokvalitet: ", + "Player volume: ": "Volym: ", + "Default comments: ": "Förvalda kommentarer: ", + "youtube": "YouTube", + "reddit": "Reddit", + "Default captions: ": "Förvalda undertexter: ", + "Fallback captions: ": "Ersättningsundertexter: ", + "Show related videos: ": "Visa relaterade videor? ", + "Show annotations by default: ": "Visa länkar-i-videon som förval? ", + "Visual preferences": "Visuella inställningar", + "Player style: ": "Spelarstil: ", + "Dark mode: ": "Mörkt läge: ", + "Theme: ": "Tema: ", + "dark": "Mörkt", + "light": "Ljust", + "Thin mode: ": "Lättviktigt läge: ", + "Subscription preferences": "Prenumerationsinställningar", + "Show annotations by default for subscribed channels: ": "Visa länkar-i-videor som förval för kanaler som prenumereras på? ", + "Redirect homepage to feed: ": "Omdirigera hemsida till flöde: ", + "Number of videos shown in feed: ": "Antal videor att visa i flödet: ", + "Sort videos by: ": "Sortera videor: ", + "published": "publicering", + "published - reverse": "publicering - omvänd", + "alphabetically": "alfabetiskt", + "alphabetically - reverse": "alfabetiskt - omvänd", + "channel name": "kanalnamn", + "channel name - reverse": "kanalnamn - omvänd", + "Only show latest video from channel: ": "Visa bara senaste videon från kanal: ", + "Only show latest unwatched video from channel: ": "Visa bara senaste osedda videon från kanal: ", + "Only show unwatched: ": "Visa bara osedda: ", + "Only show notifications (if there are any): ": "Visa endast aviseringar (om det finns några): ", + "Enable web notifications": "Slå på aviseringar", + "`x` uploaded a video": "`x` laddade upp en video", + "`x` is live": "`x` sänder live", + "Data preferences": "Datainställningar", + "Clear watch history": "Töm visningshistorik", + "Import/export data": "Importera/Exportera data", + "Change password": "Byt lösenord", + "Manage subscriptions": "Hantera prenumerationer", + "Manage tokens": "Hantera åtkomst-tokens", + "Watch history": "Visningshistorik", + "Delete account": "Radera konto", + "Administrator preferences": "Administratörsinställningar", + "Default homepage: ": "Förvald hemsida: ", + "Feed menu: ": "Flödesmeny: ", + "Top enabled: ": "Topp påslaget? ", + "CAPTCHA enabled: ": "CAPTCHA påslaget? ", + "Login enabled: ": "Inloggning påslaget? ", + "Registration enabled: ": "Registrering påslaget? ", + "Report statistics: ": "Rapportera in statistik? ", + "Save preferences": "Spara inställningar", + "Subscription manager": "Prenumerationshanterare", + "Token manager": "Åtkomst-token-hanterare", + "Token": "Åtkomst-token", + "`x` subscriptions": "`x` prenumerationer", + "`x` tokens": "`x` åtkomst-token", + "Import/export": "Importera/exportera", + "unsubscribe": "avprenumerera", + "revoke": "återkalla", + "Subscriptions": "Prenumerationer", + "`x` unseen notifications": "`x` osedda aviseringar", + "search": "sök", + "Log out": "Logga ut", + "Released under the AGPLv3 by Omar Roth.": "Utgiven under AGPLv3-licens av Omar Roth.", + "Source available here.": "Källkod tillgänglig här.", + "View JavaScript license information.": "Visa JavaScript-licensinformation.", + "View privacy policy.": "Visa privatlivspolicy.", + "Trending": "Trendar", + "Public": "Offentlig", + "Unlisted": "Olistad", + "Private": "Privat", + "View all playlists": "Visa alla spellistor", + "Updated `x` ago": "Uppdaterad `x` sedan", + "Delete playlist `x`?": "Radera spellistan `x`?", + "Delete playlist": "Radera spellista", + "Create playlist": "Skapa spellista", + "Title": "Titel", + "Playlist privacy": "Privatläge på spellista", + "Editing playlist `x`": "Redigerer spellistan `x`", + "Watch on YouTube": "Titta på YouTube", + "Hide annotations": "Dölj länkar-i-video", + "Show annotations": "Visa länkar-i-video", + "Genre: ": "Genre: ", + "License: ": "Licens: ", + "Family friendly? ": "Familjevänlig? ", + "Wilson score: ": "Wilson-poängsumma: ", + "Engagement: ": "Engagement: ", + "Whitelisted regions: ": "Vitlistade regioner: ", + "Blacklisted regions: ": "Svartlistade regioner: ", + "Shared `x`": "Delade `x`", + "`x` views": "`x` visningar", + "Premieres in `x`": "Premiär om `x`", + "Premieres `x`": "Premiär av `x`", + "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Hej. Det ser ut som att du har JavaScript avstängt. Klicka här för att visa kommentarer, ha i åtanke att nedladdning tar längre tid.", + "View YouTube comments": "Visa YouTube-kommentarer", + "View more comments on Reddit": "Visa flera kommentarer på Reddit", + "View `x` comments": "Visa `x` kommentarer", + "View Reddit comments": "Visa Reddit-kommentarer", + "Hide replies": "Dölj svar", + "Show replies": "Visa svar", + "Incorrect password": "Fel lösenord", + "Quota exceeded, try again in a few hours": "Kvoten överskriden, försök igen om ett par timmar", + "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Kunde inte logga in, försäkra dig om att tvåfaktors-autentisering (Authenticator eller SMS) är påslagen.", + "Invalid TFA code": "Ogiltig tvåfaktor-kod", + "Login failed. This may be because two-factor authentication is not turned on for your account.": "Inloggning misslyckades. Detta kan vara för att tvåfaktors-autentisering inte är påslaget på ditt konto.", + "Wrong answer": "Fel svar", + "Erroneous CAPTCHA": "Ogiltig CAPTCHA", + "CAPTCHA is a required field": "CAPTCHA är ett obligatoriskt fält", + "User ID is a required field": "Användar-ID är ett obligatoriskt fält", + "Password is a required field": "Lösenord är ett obligatoriskt fält", + "Wrong username or password": "Ogiltigt användarnamn eller lösenord", + "Please sign in using 'Log in with Google'": "Logga in genom \"Google-inloggning\"", + "Password cannot be empty": "Lösenordet kan inte vara tomt", + "Password cannot be longer than 55 characters": "Lösenordet kan inte vara längre än 55 tecken", + "Please log in": "Logga in", + "Invidious Private Feed for `x`": "Ogiltig privat flöde för `x`", + "channel:`x`": "kanal `x`", + "Deleted or invalid channel": "Raderad eller ogiltig kanal", + "This channel does not exist.": "Denna kanal finns inte.", + "Could not get channel info.": "Kunde inte hämta kanalinfo.", + "Could not fetch comments": "Kunde inte hämta kommentarer", + "View `x` replies": "Visa `x` svar", + "`x` ago": "`x` sedan", + "Load more": "Ladda fler", + "`x` points": "`x` poäng", + "Could not create mix.": "Kunde inte skapa mix.", + "Empty playlist": "Spellistan är tom", + "Not a playlist.": "Ogiltig spellista.", + "Playlist does not exist.": "Spellistan finns inte.", + "Could not pull trending pages.": "Kunde inte hämta trendande sidor.", + "Hidden field \"challenge\" is a required field": "Dolt fält \"challenge\" är ett obligatoriskt fält", + "Hidden field \"token\" is a required field": "Dolt fält \"token\" är ett obligatoriskt fält", + "Erroneous challenge": "Felaktig challenge", + "Erroneous token": "Felaktig token", + "No such user": "Ogiltig användare", + "Token is expired, please try again": "Token föråldrad, försök igen", + "English": "", + "English (auto-generated)": "English (auto-genererat)", + "Afrikaans": "", + "Albanian": "", + "Amharic": "", + "Arabic": "", + "Armenian": "", + "Azerbaijani": "", + "Bangla": "", + "Basque": "", + "Belarusian": "", + "Bosnian": "", + "Bulgarian": "", + "Burmese": "", + "Catalan": "", + "Cebuano": "", + "Chinese (Simplified)": "", + "Chinese (Traditional)": "", + "Corsican": "", + "Croatian": "", + "Czech": "", + "Danish": "", + "Dutch": "", + "Esperanto": "", + "Estonian": "", + "Filipino": "", + "Finnish": "", + "French": "", + "Galician": "", + "Georgian": "", + "German": "", + "Greek": "", + "Gujarati": "", + "Haitian Creole": "", + "Hausa": "", + "Hawaiian": "", + "Hebrew": "", + "Hindi": "", + "Hmong": "", + "Hungarian": "", + "Icelandic": "", + "Igbo": "", + "Indonesian": "", + "Irish": "", + "Italian": "", + "Japanese": "", + "Javanese": "", + "Kannada": "", + "Kazakh": "", + "Khmer": "", + "Korean": "", + "Kurdish": "", + "Kyrgyz": "", + "Lao": "", + "Latin": "", + "Latvian": "", + "Lithuanian": "", + "Luxembourgish": "", + "Macedonian": "", + "Malagasy": "", + "Malay": "", + "Malayalam": "", + "Maltese": "", + "Maori": "", + "Marathi": "", + "Mongolian": "", + "Nepali": "", + "Norwegian Bokmål": "", + "Nyanja": "", + "Pashto": "", + "Persian": "", + "Polish": "", + "Portuguese": "", + "Punjabi": "", + "Romanian": "", + "Russian": "", + "Samoan": "", + "Scottish Gaelic": "", + "Serbian": "", + "Shona": "", + "Sindhi": "", + "Sinhala": "", + "Slovak": "", + "Slovenian": "", + "Somali": "", + "Southern Sotho": "", + "Spanish": "", + "Spanish (Latin America)": "", + "Sundanese": "", + "Swahili": "", + "Swedish": "", + "Tajik": "", + "Tamil": "", + "Telugu": "", + "Thai": "", + "Turkish": "", + "Ukrainian": "", + "Urdu": "", + "Uzbek": "", + "Vietnamese": "", + "Welsh": "", + "Western Frisian": "", + "Xhosa": "", + "Yiddish": "", + "Yoruba": "", + "Zulu": "", + "`x` years": "`x` år", + "`x` months": "`x` månader", + "`x` weeks": "`x` veckor", + "`x` days": "`x` dagar", + "`x` hours": "`x` timmar", + "`x` minutes": "`x` minuter", + "`x` seconds": "`x` sekunder", + "Fallback comments: ": "Fallback-kommentarer: ", + "Popular": "Populärt", + "Top": "Topp", + "About": "Om", + "Rating: ": "Betyg: ", + "Language: ": "Språk: ", + "View as playlist": "Visa som spellista", + "Default": "Förvalt", + "Music": "Musik", + "Gaming": "Spel", + "News": "Nyheter", + "Movies": "Filmer", + "Download": "Ladda ned", + "Download as: ": "Ladda ned som: ", + "%A %B %-d, %Y": "", + "(edited)": "(redigerad)", + "YouTube comment permalink": "Permanent YouTube-länk till innehållet", + "permalink": "permalänk", + "`x` marked it with a ❤": "`x` lämnade ett ❤", + "Audio mode": "Ljudläge", + "Video mode": "Videoläge", + "Videos": "Videor", + "Playlists": "Spellistor", + "Community": "Gemenskap", + "Current version: ": "Nuvarande version: " +} From b37f51bd7f5c8bc921a72ab3e549e878746e98de Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Sat, 4 Apr 2020 15:31:24 -0500 Subject: [PATCH 0086/2930] Fix /c/ redirect --- src/invidious.cr | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index 73546d7d..3ceac127 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -3467,14 +3467,12 @@ get "/c/:user" do |env| user = env.params.url["user"] response = YT_POOL.client &.get("/c/#{user}") - document = XML.parse_html(response.body) + html = XML.parse_html(response.body) - anchor = document.xpath_node(%q(//a[contains(@class,"branded-page-header-title-link")])) - if !anchor - next env.redirect "/" - end + ucid = html.xpath_node(%q(//link[@rel="canonical"])).try &.["href"].split("/")[-1] + next env.redirect "/" if !ucid - env.redirect anchor["href"] + env.redirect "/channel/#{ucid}" end # Legacy endpoint for /user/:username From 2e378da922dfa7baa188d7c9aa0c6cf76a5d7fee Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Sat, 4 Apr 2020 15:57:29 -0500 Subject: [PATCH 0087/2930] Add support for Swedish locale --- src/invidious.cr | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/invidious.cr b/src/invidious.cr index 3ceac127..74d0c79f 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -88,10 +88,11 @@ LOCALES = { "ja" => load_locale("ja"), "nb-NO" => load_locale("nb-NO"), "nl" => load_locale("nl"), - "pt-BR" => load_locale("pt-BR"), "pl" => load_locale("pl"), + "pt-BR" => load_locale("pt-BR"), "ro" => load_locale("ro"), "ru" => load_locale("ru"), + "sv" => load_locale("sv-SE"), "tr" => load_locale("tr"), "uk" => load_locale("uk"), "zh-CN" => load_locale("zh-CN"), From 3f97bebd6956ee1b111a2c23057a4facd6cbef0a Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Tue, 7 Apr 2020 13:34:40 -0500 Subject: [PATCH 0088/2930] Support adding video to playlist from watch page --- assets/js/handlers.js | 3 +++ assets/js/playlist_widget.js | 24 +++++++++++++++++++++ src/invidious.cr | 6 ++---- src/invidious/helpers/helpers.cr | 4 +--- src/invidious/views/add_playlist_items.ecr | 2 +- src/invidious/views/playlist.ecr | 2 +- src/invidious/views/watch.ecr | 25 ++++++++++++++++++++++ 7 files changed, 57 insertions(+), 9 deletions(-) diff --git a/assets/js/handlers.js b/assets/js/handlers.js index 7ecb5a02..b3da8d9b 100644 --- a/assets/js/handlers.js +++ b/assets/js/handlers.js @@ -55,6 +55,9 @@ n2a(document.querySelectorAll('[data-onclick="mark_unwatched"]')).forEach(function (e) { e.onclick = function () { mark_unwatched(e); }; }); + n2a(document.querySelectorAll('[data-onclick="add_playlist_video"]')).forEach(function (e) { + e.onclick = function () { add_playlist_video(e); }; + }); n2a(document.querySelectorAll('[data-onclick="add_playlist_item"]')).forEach(function (e) { e.onclick = function () { add_playlist_item(e); }; }); diff --git a/assets/js/playlist_widget.js b/assets/js/playlist_widget.js index a29d7ef0..0ec27859 100644 --- a/assets/js/playlist_widget.js +++ b/assets/js/playlist_widget.js @@ -1,5 +1,29 @@ var playlist_data = JSON.parse(document.getElementById('playlist_data').innerHTML); +function add_playlist_video(target) { + var select = target.parentNode.children[0].children[1]; + var option = select.children[select.selectedIndex]; + + var url = '/playlist_ajax?action_add_video=1&redirect=false' + + '&video_id=' + target.getAttribute('data-id') + + '&playlist_id=' + option.getAttribute('data-plid'); + var xhr = new XMLHttpRequest(); + xhr.responseType = 'json'; + xhr.timeout = 10000; + xhr.open('POST', url, true); + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + + xhr.onreadystatechange = function () { + if (xhr.readyState == 4) { + if (xhr.status == 200) { + option.innerText = '✓' + option.innerText; + } + } + } + + xhr.send('csrf_token=' + playlist_data.csrf_token); +} + function add_playlist_item(target) { var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode; tile.style.display = 'none'; diff --git a/src/invidious.cr b/src/invidious.cr index 74d0c79f..1448c502 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -3131,9 +3131,7 @@ get "/feed/channel/:ucid" do |env| rss = YT_POOL.client &.get("/feeds/videos.xml?channel_id=#{channel.ucid}").body rss = XML.parse_html(rss) - videos = [] of SearchVideo - - rss.xpath_nodes("//feed/entry").each do |entry| + videos = rss.xpath_nodes("//feed/entry").map do |entry| video_id = entry.xpath_node("videoid").not_nil!.content title = entry.xpath_node("title").not_nil!.content @@ -3145,7 +3143,7 @@ get "/feed/channel/:ucid" do |env| description_html = entry.xpath_node("group/description").not_nil!.to_s views = entry.xpath_node("group/community/statistics").not_nil!.["views"].to_i64 - videos << SearchVideo.new( + SearchVideo.new( title: title, id: video_id, author: author, diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr index 2341d3be..e168c55e 100644 --- a/src/invidious/helpers/helpers.cr +++ b/src/invidious/helpers/helpers.cr @@ -732,9 +732,7 @@ def cache_annotation(db, id, annotations) body = XML.parse(annotations) nodeset = body.xpath_nodes(%q(/document/annotations/annotation)) - if nodeset == 0 - return - end + return if nodeset == 0 has_legacy_annotations = false nodeset.each do |node| diff --git a/src/invidious/views/add_playlist_items.ecr b/src/invidious/views/add_playlist_items.ecr index 07295c1a..09eacbc8 100644 --- a/src/invidious/views/add_playlist_items.ecr +++ b/src/invidious/views/add_playlist_items.ecr @@ -27,7 +27,7 @@ }.to_pretty_json %> - +
<% videos.each_slice(4) do |slice| %> diff --git a/src/invidious/views/playlist.ecr b/src/invidious/views/playlist.ecr index ccda94d9..7316af14 100644 --- a/src/invidious/views/playlist.ecr +++ b/src/invidious/views/playlist.ecr @@ -76,7 +76,7 @@ }.to_pretty_json %> - + <% end %>
diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 2a99dd5b..e43282cb 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -101,6 +101,31 @@ <% end %>

+ <% if user %> +
+
+ + +
+ + +
+ + + <% end %> + <% if CONFIG.dmca_content.includes?(video.id) || CONFIG.disabled?("downloads") %>

<%= translate(locale, "Download is disabled.") %>

<% else %> From 02d4186b110bb5cf8cc07672a4ff45f7189eb3e6 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Thu, 9 Apr 2020 10:53:00 -0500 Subject: [PATCH 0089/2930] Fix player matching --- src/invidious/helpers/signatures.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/helpers/signatures.cr b/src/invidious/helpers/signatures.cr index ab864f03..f82cc8dd 100644 --- a/src/invidious/helpers/signatures.cr +++ b/src/invidious/helpers/signatures.cr @@ -2,7 +2,7 @@ alias SigProc = Proc(Array(String), Int32, Array(String)) def fetch_decrypt_function(id = "CvFH_6DNRCY") document = YT_POOL.client &.get("/watch?v=#{id}&gl=US&hl=en&disable_polymer=1").body - url = document.match(/src="(?\/yts\/jsbin\/player_ias-[^\/]+\/en_US\/base.js)"/).not_nil!["url"] + url = document.match(/src="(?.*player_ias[^\/]+\/en_US\/base.js)"/).not_nil!["url"] player = YT_POOL.client &.get(url).body function_name = player.match(/^(?[^=]+)=function\(\w\){\w=\w\.split\(""\);[^\. ]+\.[^( ]+/m).not_nil!["name"] From be655ee3289f7564732bd6fefe20bce9d433cd3c Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Thu, 9 Apr 2020 11:14:21 -0500 Subject: [PATCH 0090/2930] Bump dependencies --- shard.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/shard.yml b/shard.yml index 59f5607b..f3668ff4 100644 --- a/shard.yml +++ b/shard.yml @@ -11,10 +11,10 @@ targets: dependencies: pg: github: will/crystal-pg - version: ~> 0.19.0 + version: ~> 0.21.0 sqlite3: github: crystal-lang/crystal-sqlite3 - version: ~> 0.14.0 + version: ~> 0.16.0 kemal: github: kemalcr/kemal version: ~> 0.26.1 @@ -28,6 +28,6 @@ dependencies: github: omarroth/lsquic.cr version: ~> 0.1.9 -crystal: 0.33.0 +crystal: 0.34.0 license: AGPLv3 From ca1185d0be46e275688af705994ad64364d5b778 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Thu, 9 Apr 2020 12:18:09 -0500 Subject: [PATCH 0091/2930] Fix warnings in latest version of Crystal --- src/invidious.cr | 32 +++++++- src/invidious/channels.cr | 3 + src/invidious/comments.cr | 3 + src/invidious/helpers/helpers.cr | 12 ++- src/invidious/helpers/i18n.cr | 2 + src/invidious/helpers/tokens.cr | 11 +-- src/invidious/helpers/utils.cr | 3 +- src/invidious/search.cr | 3 + src/invidious/users.cr | 2 + src/invidious/videos.cr | 1 + src/invidious/views/login.ecr | 126 +++++++++++++++---------------- 11 files changed, 119 insertions(+), 79 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index 1448c502..55974e4a 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -385,6 +385,8 @@ get "/" do |env| else templated "popular" end + else + templated "empty" end end @@ -722,6 +724,7 @@ get "/embed/:id" do |env| end next env.redirect url + else nil # Continue end params = process_video_params(env.params.query, preferences) @@ -1213,6 +1216,10 @@ post "/playlist_ajax" do |env| error_message = {"error" => "Playlist cannot have more than 500 videos"}.to_json next error_message end + else + error_message = {"error" => "Unsupported action #{action}"}.to_json + env.response.status_code = 400 + next error_message end video_id = env.params.query["video_id"] @@ -1253,6 +1260,10 @@ post "/playlist_ajax" do |env| PG_DB.exec("UPDATE playlists SET index = array_remove(index, $1), video_count = cardinality(index), updated = $2 WHERE id = $3", index, Time.utc, playlist_id) when "action_move_video_before" # TODO: Playlist stub + else + error_message = {"error" => "Unsupported action #{action}"}.to_json + env.response.status_code = 400 + next error_message end if redirect @@ -1547,7 +1558,7 @@ post "/login" do |env| case prompt_type when "TWO_STEP_VERIFICATION" prompt_type = 2 - when "LOGIN_CHALLENGE" + else # "LOGIN_CHALLENGE" prompt_type = 4 end @@ -1840,7 +1851,7 @@ post "/login" do |env| env.response.status_code = 400 next templated "error" end - when "text" + else # "text" answer = Digest::MD5.hexdigest(answer.downcase.strip) found_valid_captcha = false @@ -2251,6 +2262,10 @@ post "/watch_ajax" do |env| end when "action_mark_unwatched" PG_DB.exec("UPDATE users SET watched = array_remove(watched, $1) WHERE email = $2", id, user.email) + else + error_message = {"error" => "Unsupported action #{action}"}.to_json + env.response.status_code = 400 + next error_message end if redirect @@ -2405,6 +2420,10 @@ post "/subscription_ajax" do |env| end when "action_remove_subscriptions" PG_DB.exec("UPDATE users SET feed_needs_update = true, subscriptions = array_remove(subscriptions, $1) WHERE email = $2", channel_id, email) + else + error_message = {"error" => "Unsupported action #{action}"}.to_json + env.response.status_code = 400 + next error_message end if redirect @@ -2559,6 +2578,7 @@ post "/data_control" do |env| next end + # TODO: Unify into single import based on content-type case part.name when "import_invidious" body = JSON.parse(body) @@ -2645,6 +2665,7 @@ post "/data_control" do |env| end end end + else nil # Ignore end end end @@ -2986,6 +3007,10 @@ post "/token_ajax" do |env| case action when .starts_with? "action_revoke_token" PG_DB.exec("DELETE FROM session_ids * WHERE id = $1 AND email = $2", session, user.email) + else + error_message = {"error" => "Unsupported action #{action}"}.to_json + env.response.status_code = 400 + next error_message end if redirect @@ -3280,6 +3305,7 @@ get "/feed/playlist/:plid" do |env| full_path = URI.parse(node[attribute.name]).full_path query_string_opt = full_path.starts_with?("/watch?v=") ? "&#{params}" : "" node[attribute.name] = "#{host_url}#{full_path}#{query_string_opt}" + else nil # Skip end end end @@ -4037,7 +4063,7 @@ get "/api/v1/annotations/:id" do |env| cache_annotation(PG_DB, id, annotations) end - when "youtube" + else # "youtube" response = YT_POOL.client &.get("/annotations_invideo?video_id=#{id}") if response.status_code != 200 diff --git a/src/invidious/channels.cr b/src/invidious/channels.cr index 35ef5df2..afc1528e 100644 --- a/src/invidious/channels.cr +++ b/src/invidious/channels.cr @@ -412,6 +412,7 @@ def fetch_channel_playlists(ucid, author, auto_generated, continuation, sort_by) url += "&sort=da" when "newest", "newest_created" url += "&sort=dd" + else nil # Ignore end response = YT_POOL.client &.get(url) @@ -469,6 +470,7 @@ def produce_channel_videos_url(ucid, page = 1, auto_generated = nil, sort_by = " object["80226972:embedded"]["3:base64"].as(Hash)["3:varint"] = 0x01_i64 when "oldest" object["80226972:embedded"]["3:base64"].as(Hash)["3:varint"] = 0x02_i64 + else nil # Ignore end object["80226972:embedded"]["3:string"] = Base64.urlsafe_encode(Protodec::Any.from_json(Protodec::Any.cast_json(object["80226972:embedded"]["3:base64"]))) @@ -513,6 +515,7 @@ def produce_channel_playlists_url(ucid, cursor, sort = "newest", auto_generated object["80226972:embedded"]["3:base64"].as(Hash)["3:varint"] = 3_i64 when "last", "last_added" object["80226972:embedded"]["3:base64"].as(Hash)["3:varint"] = 4_i64 + else nil # Ignore end end diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr index 4a048d7a..24564bb9 100644 --- a/src/invidious/comments.cr +++ b/src/invidious/comments.cr @@ -356,6 +356,7 @@ def template_youtube_comments(comments, locale, thin_mode)
END_HTML + else nil # Ignore end end @@ -609,6 +610,8 @@ def produce_comment_continuation(video_id, cursor = "", sort_by = "top") object["6:embedded"].as(Hash)["4:embedded"].as(Hash)["6:varint"] = 0_i64 when "new", "newest" object["6:embedded"].as(Hash)["4:embedded"].as(Hash)["6:varint"] = 1_i64 + else # top + object["6:embedded"].as(Hash)["4:embedded"].as(Hash)["6:varint"] = 0_i64 end continuation = object.try { |i| Protodec::Any.cast_json(object) } diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr index e168c55e..96d14737 100644 --- a/src/invidious/helpers/helpers.cr +++ b/src/invidious/helpers/helpers.cr @@ -173,6 +173,8 @@ struct Config yaml.scalar "ipv4" when Socket::Family::INET6 yaml.scalar "ipv6" + when Socket::Family::UNIX + raise "Invalid socket family #{value}" end end @@ -223,6 +225,8 @@ struct Config else return false end + else + return false end end @@ -520,9 +524,7 @@ def extract_shelf_items(nodeset, ucid = nil, author_name = nil) shelf.xpath_nodes(%q(.//ul[contains(@class, "yt-uix-shelfslider-list") or contains(@class, "expanded-shelf-content-list")]/li)).each do |child_node| type = child_node.xpath_node(%q(./div)) - if !type - next - end + next if !type case type["class"] when .includes? "yt-lockup-video" @@ -599,6 +601,8 @@ def extract_shelf_items(nodeset, ucid = nil, author_name = nil) videos: videos, thumbnail: playlist_thumbnail ) + else + next # Skip end end @@ -763,7 +767,7 @@ def create_notification_stream(env, config, kemal_config, decrypt_function, topi loop do time_span = [0, 0, 0, 0] time_span[rand(4)] = rand(30) + 5 - published = Time.utc - Time::Span.new(time_span[0], time_span[1], time_span[2], time_span[3]) + published = Time.utc - Time::Span.new(days: time_span[0], hours: time_span[1], minutes: time_span[2], seconds: time_span[3]) video_id = TEST_IDS[rand(TEST_IDS.size)] video = get_video(video_id, PG_DB) diff --git a/src/invidious/helpers/i18n.cr b/src/invidious/helpers/i18n.cr index 4c9bb2d6..0faa2e32 100644 --- a/src/invidious/helpers/i18n.cr +++ b/src/invidious/helpers/i18n.cr @@ -24,6 +24,8 @@ def translate(locale : Hash(String, JSON::Any) | Nil, translation : String, text if !locale[translation].as_s.empty? translation = locale[translation].as_s end + else + raise "Invalid translation #{translation}" end end diff --git a/src/invidious/helpers/tokens.cr b/src/invidious/helpers/tokens.cr index 0b609e80..39aae367 100644 --- a/src/invidious/helpers/tokens.cr +++ b/src/invidious/helpers/tokens.cr @@ -43,15 +43,10 @@ def sign_token(key, hash) string_to_sign = [] of String hash.each do |key, value| - if key == "signature" - next - end + next if key == "signature" - if value.is_a?(JSON::Any) - case value - when .as_a? - value = value.as_a.map { |item| item.as_s } - end + if value.is_a?(JSON::Any) && value.as_a? + value = value.as_a.map { |i| i.as_s } end case value diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index d0892862..79a69cf9 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -100,7 +100,7 @@ end def decode_length_seconds(string) length_seconds = string.gsub(/[^0-9:]/, "").split(":").map &.to_i length_seconds = [0] * (3 - length_seconds.size) + length_seconds - length_seconds = Time::Span.new(length_seconds[0], length_seconds[1], length_seconds[2]) + length_seconds = Time::Span.new hours: length_seconds[0], minutes: length_seconds[1], seconds: length_seconds[2] length_seconds = length_seconds.total_seconds.to_i return length_seconds @@ -162,6 +162,7 @@ def decode_date(string : String) return Time.utc when "yesterday" return Time.utc - 1.day + else nil # Continue end # String matches format "20 hours ago", "4 months ago"... diff --git a/src/invidious/search.cr b/src/invidious/search.cr index 92996f75..e8521629 100644 --- a/src/invidious/search.cr +++ b/src/invidious/search.cr @@ -310,6 +310,7 @@ def produce_search_params(sort : String = "relevance", date : String = "", conte object["2:embedded"].as(Hash)["1:varint"] = 4_i64 when "year" object["2:embedded"].as(Hash)["1:varint"] = 5_i64 + else nil # Ignore end case content_type @@ -334,6 +335,7 @@ def produce_search_params(sort : String = "relevance", date : String = "", conte object["2:embedded"].as(Hash)["3:varint"] = 1_i64 when "long" object["2:embedded"].as(Hash)["3:varint"] = 2_i64 + else nil # Ignore end features.each do |feature| @@ -358,6 +360,7 @@ def produce_search_params(sort : String = "relevance", date : String = "", conte object["2:embedded"].as(Hash)["23:varint"] = 1_i64 when "hdr" object["2:embedded"].as(Hash)["25:varint"] = 1_i64 + else nil # Ignore end end diff --git a/src/invidious/users.cr b/src/invidious/users.cr index afb100f2..0aa94d82 100644 --- a/src/invidious/users.cr +++ b/src/invidious/users.cr @@ -350,6 +350,7 @@ def get_subscription_feed(db, user, max_results = 40, page = 1) notifications.sort_by! { |video| video.author } when "channel name - reverse" notifications.sort_by! { |video| video.author }.reverse! + else nil # Ignore end else if user.preferences.latest_only @@ -398,6 +399,7 @@ def get_subscription_feed(db, user, max_results = 40, page = 1) videos.sort_by! { |video| video.author } when "channel name - reverse" videos.sort_by! { |video| video.author }.reverse! + else nil # Ignore end notifications = PG_DB.query_one("SELECT notifications FROM users WHERE email = $1", user.email, as: Array(String)) diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index 1c7599f8..f9d3dc28 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -1250,6 +1250,7 @@ def fetch_video(id, region) genre_url = "/channel/UCfFyYRYslvuhwMDnx6KjUvw" when "Trailers" genre_url = "/channel/UClgRkhTL3_hImCAmdLfDE4g" + else nil # Ignore end license = html.xpath_node(%q(//h4[contains(text(),"License")]/parent::*/ul/li)).try &.content || "" diff --git a/src/invidious/views/login.ecr b/src/invidious/views/login.ecr index 59fa90e5..b6e8117b 100644 --- a/src/invidious/views/login.ecr +++ b/src/invidious/views/login.ecr @@ -22,69 +22,6 @@
<% case account_type when %> - <% when "invidious" %> -
-
- <% if email %> - - <% else %> - - "> - <% end %> - - <% if password %> - - <% else %> - - "> - <% end %> - - <% if captcha %> - <% case captcha_type when %> - <% when "image" %> - <% captcha = captcha.not_nil! %> - - <% captcha[:tokens].each_with_index do |token, i| %> - - <% end %> - - - - <% when "text" %> - <% captcha = captcha.not_nil! %> - <% captcha[:tokens].each_with_index do |token, i| %> - - <% end %> - - - "> - <% end %> - - - - <% case captcha_type when %> - <% when "image" %> - - <% when "text" %> - - <% end %> - <% else %> - - <% end %> -
-
<% when "google" %>
@@ -121,6 +58,69 @@
+ <% else # "invidious" %> +
+
+ <% if email %> + + <% else %> + + "> + <% end %> + + <% if password %> + + <% else %> + + "> + <% end %> + + <% if captcha %> + <% case captcha_type when %> + <% when "image" %> + <% captcha = captcha.not_nil! %> + + <% captcha[:tokens].each_with_index do |token, i| %> + + <% end %> + + + + <% else # "text" %> + <% captcha = captcha.not_nil! %> + <% captcha[:tokens].each_with_index do |token, i| %> + + <% end %> + + + "> + <% end %> + + + + <% case captcha_type when %> + <% when "image" %> + + <% else # "text" %> + + <% end %> + <% else %> + + <% end %> +
+
<% end %>
From 920463f2ff04f4e749db5bf8d0607c7bfe4fb5eb Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Fri, 10 Apr 2020 11:49:18 -0500 Subject: [PATCH 0092/2930] Fix playlist_ajax --- src/invidious.cr | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index 55974e4a..754d1513 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -1216,10 +1216,6 @@ post "/playlist_ajax" do |env| error_message = {"error" => "Playlist cannot have more than 500 videos"}.to_json next error_message end - else - error_message = {"error" => "Unsupported action #{action}"}.to_json - env.response.status_code = 400 - next error_message end video_id = env.params.query["video_id"] From 7bb7003c9daa933f036d3d832b91d5b2c558ed98 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Fri, 10 Apr 2020 11:49:51 -0500 Subject: [PATCH 0093/2930] Fix authorThumbnails in /api/v1/channels --- src/invidious.cr | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index 754d1513..2d72f49f 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -3149,8 +3149,8 @@ get "/feed/channel/:ucid" do |env| next error_message end - rss = YT_POOL.client &.get("/feeds/videos.xml?channel_id=#{channel.ucid}").body - rss = XML.parse_html(rss) + response = YT_POOL.client &.get("/feeds/videos.xml?channel_id=#{channel.ucid}") + rss = XML.parse_html(response.body) videos = rss.xpath_nodes("//feed/entry").map do |entry| video_id = entry.xpath_node("videoid").not_nil!.content @@ -4259,7 +4259,7 @@ get "/api/v1/channels/:ucid" do |env| qualities.each do |quality| json.object do - json.field "url", channel.author_thumbnail.gsub(/=\d+/, "=s#{quality}") + json.field "url", channel.author_thumbnail.gsub(/=s\d+/, "=s#{quality}") json.field "width", quality json.field "height", quality end From 61150c74d21bc98e4b819602bbca67ca23b82dc0 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Tue, 14 Apr 2020 18:08:58 -0500 Subject: [PATCH 0094/2930] Move privacy type into playlists.sql --- README.md | 1 - config/sql/playlists.sql | 11 +++++++++++ config/sql/privacy.sql | 10 ---------- docker/entrypoint.postgres.sh | 1 - kubernetes/README.md | 1 - 5 files changed, 11 insertions(+), 13 deletions(-) delete mode 100644 config/sql/privacy.sql diff --git a/README.md b/README.md index 3d453208..d8cc1c6e 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,6 @@ $ psql invidious kemal < /home/invidious/invidious/config/sql/users.sql $ psql invidious kemal < /home/invidious/invidious/config/sql/session_ids.sql $ psql invidious kemal < /home/invidious/invidious/config/sql/nonces.sql $ psql invidious kemal < /home/invidious/invidious/config/sql/annotations.sql -$ psql invidious kemal < /home/invidious/invidious/config/sql/privacy.sql $ psql invidious kemal < /home/invidious/invidious/config/sql/playlists.sql $ psql invidious kemal < /home/invidious/invidious/config/sql/playlist_videos.sql $ exit diff --git a/config/sql/playlists.sql b/config/sql/playlists.sql index 46ff30ec..468496cb 100644 --- a/config/sql/playlists.sql +++ b/config/sql/playlists.sql @@ -1,3 +1,14 @@ +-- Type: public.privacy + +-- DROP TYPE public.privacy; + +CREATE TYPE public.privacy AS ENUM +( + 'Public', + 'Unlisted', + 'Private' +); + -- Table: public.playlists -- DROP TABLE public.playlists; diff --git a/config/sql/privacy.sql b/config/sql/privacy.sql deleted file mode 100644 index 4356813e..00000000 --- a/config/sql/privacy.sql +++ /dev/null @@ -1,10 +0,0 @@ --- Type: public.privacy - --- DROP TYPE public.privacy; - -CREATE TYPE public.privacy AS ENUM -( - 'Public', - 'Unlisted', - 'Private' -); diff --git a/docker/entrypoint.postgres.sh b/docker/entrypoint.postgres.sh index 1588c56c..be6f6782 100755 --- a/docker/entrypoint.postgres.sh +++ b/docker/entrypoint.postgres.sh @@ -19,7 +19,6 @@ if [ ! -f /var/lib/postgresql/data/setupFinished ]; then su postgres -c 'psql invidious kemal < config/sql/session_ids.sql' su postgres -c 'psql invidious kemal < config/sql/nonces.sql' su postgres -c 'psql invidious kemal < config/sql/annotations.sql' - su postgres -c 'psql invidious kemal < config/sql/privacy.sql' su postgres -c 'psql invidious kemal < config/sql/playlists.sql' su postgres -c 'psql invidious kemal < config/sql/playlist_videos.sql' touch /var/lib/postgresql/data/setupFinished diff --git a/kubernetes/README.md b/kubernetes/README.md index 1c62f469..35478f99 100644 --- a/kubernetes/README.md +++ b/kubernetes/README.md @@ -17,7 +17,6 @@ $ kubectl create configmap invidious-postgresql-init \ --from-file=../config/sql/session_ids.sql \ --from-file=../config/sql/nonces.sql \ --from-file=../config/sql/annotations.sql \ - --from-file=../config/sql/privacy.sql \ --from-file=../config/sql/playlists.sql \ --from-file=../config/sql/playlist_videos.sql From 408f3852ec1a6987bb72df6efe6e77f8613c560a Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Wed, 15 Apr 2020 16:30:02 -0500 Subject: [PATCH 0095/2930] Hide playlist widget when user has no playlists --- src/invidious/views/watch.ecr | 45 +++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index e43282cb..7743fd4a 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -102,28 +102,31 @@

<% if user %> -
-
- - -
+ <% playlists = PG_DB.query_all("SELECT id,title FROM playlists WHERE author = $1", user.email, as: {String, String}) %> + <% if !playlists.empty? %> + +
+ + +
- -
- - + + + + + <% end %> <% end %> <% if CONFIG.dmca_content.includes?(video.id) || CONFIG.disabled?("downloads") %> From 21a00b77bddcc2d318d763b573e175815c8f5e83 Mon Sep 17 00:00:00 2001 From: Tamas Cservenak Date: Mon, 20 Apr 2020 23:05:28 +0200 Subject: [PATCH 0096/2930] Add Hungarian translation (#1111) --- locales/hu-HU.json | 335 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 335 insertions(+) create mode 100644 locales/hu-HU.json diff --git a/locales/hu-HU.json b/locales/hu-HU.json new file mode 100644 index 00000000..29a35da8 --- /dev/null +++ b/locales/hu-HU.json @@ -0,0 +1,335 @@ +{ + "`x` subscribers": "`x` feliratkozó", + "`x` videos": "`x` videó", + "`x` playlists": "`x` playlist", + "LIVE": "ÉLŐ", + "Shared `x` ago": "`x` óta megosztva", + "Unsubscribe": "Leiratkozás", + "Subscribe": "Feliratkozás", + "View channel on YouTube": "Csatokrna megtekintése a YouTube-on", + "View playlist on YouTube": "Playlist megtekintése a YouTube-on", + "newest": "legújabb", + "oldest": "legrégibb", + "popular": "népszerű", + "last": "utolsó", + "Next page": "Következő oldal", + "Previous page": "Előző oldal", + "Clear watch history?": "Megtekintési napló törlése?", + "New password": "Új jelszó", + "New passwords must match": "Az új jelszavaknak egyezniük kell", + "Cannot change password for Google accounts": "Google fiók jelszavát nem lehet cserélni", + "Authorize token?": "Token felhatalmazása?", + "Authorize token for `x`?": "Token felhatalmazása `x`-ra?", + "Yes": "Igen", + "No": "Nem", + "Import and Export Data": "Adatok importálása és exportálása", + "Import": "Importálás", + "Import Invidious data": "Invidious adatainak importálása", + "Import YouTube subscriptions": "YouTube feliratkozások importálása", + "Import FreeTube subscriptions (.db)": "FreeTube feliratkozások importálása (.db)", + "Import NewPipe subscriptions (.json)": "NewPipe feliratkozások importálása (.json)", + "Import NewPipe data (.zip)": "NewPipe adatainak importálása (.zip)", + "Export": "Exportálás", + "Export subscriptions as OPML": "Feliratkozások exportálása OPML-ként", + "Export subscriptions as OPML (for NewPipe & FreeTube)": "Feliratkozások exportálása OPML-ként (NewPipe és FreeTube számára)", + "Export data as JSON": "Adat exportálása JSON-ként", + "Delete account?": "Fiók törlése?", + "History": "Megtekintési napló", + "An alternative front-end to YouTube": "Alternatív YouTube front-end", + "JavaScript license information": "JavaScript licensz információ", + "source": "forrás", + "Log in": "Bejelentkezés", + "Log in/register": "Bejelentkezés/Regisztráció", + "Log in with Google": "Bejelentkezés Google fiókkal", + "User ID": "Felhasználó-ID", + "Password": "Jelszó", + "Time (h:mm:ss):": "Idő (h:mm:ss):", + "Text CAPTCHA": "Szöveg-CAPTCHA", + "Image CAPTCHA": "Kép-CAPTCHA", + "Sign In": "Bejelentkezés", + "Register": "Regisztráció", + "E-mail": "E-mail", + "Google verification code": "Google verifikációs kód", + "Preferences": "Beállítások", + "Player preferences": "Lejátszó beállítások", + "Always loop: ": "Mindig loop-ol: ", + "Autoplay: ": "Automatikus lejátszás: ", + "Play next by default: ": "Következő lejátszása alapértelmezésben: ", + "Autoplay next video: ": "Következő automatikus lejátszása: ", + "Listen by default: ": "Hallgatás alapértelmezésben: ", + "Proxy videos: ": "Proxy videók: ", + "Default speed: ": "Alapértelmezett sebesség: ", + "Preferred video quality: ": "Kívánt video minőség: ", + "Player volume: ": "Hangerő: ", + "Default comments: ": "Alapértelmezett kommentek: ", + "youtube": "YouTube", + "reddit": "Reddit", + "Default captions: ": "Alapértelmezett feliratok: ", + "Fallback captions: ": "Másodlagos feliratok: ", + "Show related videos: ": "Kapcsolódó videók mutatása: ", + "Show annotations by default: ": "Annotációk mutatása alapértelmetésben: ", + "Visual preferences": "Vizuális preferenciák", + "Player style: ": "Lejátszó stílusa: ", + "Dark mode: ": "Sötét mód: ", + "Theme: ": "Téma: ", + "dark": "Sötét", + "light": "Világos", + "Thin mode: ": "Vékony mód: ", + "Subscription preferences": "Feliratkozási beállítások", + "Show annotations by default for subscribed channels: ": "Annotációk mutatása alapértelmezésben feliratkozott csatornák esetében: ", + "Redirect homepage to feed: ": "Kezdő oldal átirányitása a feed-re: ", + "Number of videos shown in feed: ": "Feed-ben mutatott videók száma: ", + "Sort videos by: ": "Videók sorrendje: ", + "published": "közzétéve", + "published - reverse": "közzétéve (ford.)", + "alphabetically": "ABC sorrend", + "alphabetically - reverse": "ABC sorrend (ford.)", + "channel name": "csatorna neve", + "channel name - reverse": "csatorna neve (ford.)", + "Only show latest video from channel: ": "Csak a legutolsó videó mutatása a csatornából: ", + "Only show latest unwatched video from channel: ": "Csak a legutolsó nem megtekintett videó mutatása a csatornából: ", + "Only show unwatched: ": "Csak a nem megtekintettek mutatása: ", + "Only show notifications (if there are any): ": "Csak értesítések mutatása (ha van): ", + "Enable web notifications": "Web értesítések bekapcsolása", + "`x` uploaded a video": "`x` feltöltött egy videót", + "`x` is live": "`x` élő", + "Data preferences": "Adat beállítások", + "Clear watch history": "Megtekintési napló törlése", + "Import/export data": "Adat Import/Export", + "Change password": "Jelszócsere", + "Manage subscriptions": "Feliratkozások kezelése", + "Manage tokens": "Tokenek kezelése", + "Watch history": "Megtekintési napló", + "Delete account": "Fiók törlése", + "Administrator preferences": "Adminisztrátor beállítások", + "Default homepage: ": "Alapértelmezett honlap: ", + "Feed menu: ": "Feed menü: ", + "Top enabled: ": "Top lista engedélyezve: ", + "CAPTCHA enabled: ": "CAPTCHA engedélyezve: ", + "Login enabled: ": "Bejelentkezés engedélyezve: ", + "Registration enabled: ": "Registztráció engedélyezve: ", + "Report statistics: ": "Statisztikák gyűjtése: ", + "Save preferences": "Beállítások mentése", + "Subscription manager": "Feliratkozás kezelő", + "Token manager": "Token kezelő", + "Token": "Token", + "`x` subscriptions": "`x` feliratkozás", + "`x` tokens": "`x` token", + "Import/export": "Import/export", + "unsubscribe": "leiratkozás", + "revoke": "visszavonás", + "Subscriptions": "Feliratkozások", + "`x` unseen notifications": "`x` kimaradt érdesítés", + "search": "keresés", + "Log out": "Kijelentkezés", + "Released under the AGPLv3 by Omar Roth.": "Omar Roth által release-elve AGPLv3 licensz alatt.", + "Source available here.": "Forrás elérhető itt.", + "View JavaScript license information.": "JavaScript licensz inforkációk megtekintése.", + "View privacy policy.": "Adatvédelem irányelv megtekintése.", + "Trending": "Trending", + "Public": "Nyilvános", + "Unlisted": "Nem nyilvános", + "Private": "Privát", + "View all playlists": "Minden playlist megtekintése", + "Updated `x` ago": "Frissitve `x`", + "Delete playlist `x`?": "`x` playlist törlése?", + "Delete playlist": "Playlist törlése", + "Create playlist": "Playlist létrehozása", + "Title": "Címe", + "Playlist privacy": "Playlist láthatósága", + "Editing playlist `x`": "`x` playlist szerkesztése", + "Watch on YouTube": "Megtekintés a YouTube-on", + "Hide annotations": "Annotációk elrejtése", + "Show annotations": "Annotációk mutatása", + "Genre: ": "Zsáner: ", + "License: ": "Licensz: ", + "Family friendly? ": "Családbarát? ", + "Wilson score: ": "Wilson-ponstszém: ", + "Engagement: ": "Engagement: ", + "Whitelisted regions: ": "Engedélyezett régiók: ", + "Blacklisted regions: ": "Tiltott régiók: ", + "Shared `x`": "Megosztva `x`", + "`x` views": "`x` megtekintés", + "Premieres in `x`": "Premier `x`", + "Premieres `x`": "Premier `x`", + "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.", + "View YouTube comments": "YouTube kommentek megtekintése", + "View more comments on Reddit": "További Reddit kommentek megtekintése", + "View `x` comments": "`x` komment megtekintése", + "View Reddit comments": "Reddit kommentek megtekintése", + "Hide replies": "Válaszok elrejtése", + "Show replies": "Válaszok mutatása", + "Incorrect password": "Helytelen jelszó", + "Quota exceeded, try again in a few hours": "Kvóta túllépve, próbálkozz pár órával később", + "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Sikertelen belépés, győződj meg róla hogy a 2FA (Authenticator vagy SMS) engedélyezve van.", + "Login failed. This may be because two-factor authentication is not turned on for your account.": "Sikertelen belépés, győződj meg róla hogy a 2FA (Authenticator vagy SMS) engedélyezve van.", + "Wrong answer": "Rossz válasz", + "Erroneous CAPTCHA": "Hibás CAPTCHA", + "CAPTCHA is a required field": "A CAPTCHA kötelező", + "User ID is a required field": "A felhasználó-ID kötelező", + "Password is a required field": "A jelszó kötelező", + "Wrong username or password": "Rossz felhasználónév vagy jelszó", + "Please sign in using 'Log in with Google'": "Kérem, jelentkezzen be a \"Bejelentkezés Google-el\"", + "Password cannot be empty": "A jelszó nem lehet üres", + "Password cannot be longer than 55 characters": "A jelszó nem lehet hosszabb 55 betűnél", + "Please log in": "Kérem lépjen be", + "Invidious Private Feed for `x`": "`x` Invidious privát feed-je", + "channel:`x`": "`x` csatorna", + "Deleted or invalid channel": "Törölt vagy nemlétező csatorna", + "This channel does not exist.": "Ez a csatorna nem létezik.", + "Could not get channel info.": "Nem megszerezhető a csatorna információ.", + "Could not fetch comments": "Nem megszerezhetőek a kommentek", + "View `x` replies": "`x` válasz megtekintése", + "`x` ago": "`x` óta", + "Load more": "További betöltése", + "`x` points": "`x` pont", + "Could not create mix.": "Nem tudok mix-et készíteni.", + "Empty playlist": "Üres playlist", + "Not a playlist.": "Nem playlist.", + "Playlist does not exist.": "Nem létező playlist.", + "Could not pull trending pages.": "Nem tudom letölteni a trendek adatait.", + "Hidden field \"challenge\" is a required field": "A rejtett \"challenge\" mező kötelező", + "Hidden field \"token\" is a required field": "A rejtett \"token\" mező kötelező", + "Erroneous challenge": "Hibás challenge", + "Erroneous token": "Hibás token", + "No such user": "Nincs ilyen felhasználó", + "Token is expired, please try again": "Lejárt token, kérem próbáld újra", + "English": "", + "English (auto-generated)": "English (auto-genererat)", + "Afrikaans": "", + "Albanian": "", + "Amharic": "", + "Arabic": "", + "Armenian": "", + "Azerbaijani": "", + "Bangla": "", + "Basque": "", + "Belarusian": "", + "Bosnian": "", + "Bulgarian": "", + "Burmese": "", + "Catalan": "", + "Cebuano": "", + "Chinese (Simplified)": "", + "Chinese (Traditional)": "", + "Corsican": "", + "Croatian": "", + "Czech": "", + "Danish": "", + "Dutch": "", + "Esperanto": "", + "Estonian": "", + "Filipino": "", + "Finnish": "", + "French": "", + "Galician": "", + "Georgian": "", + "German": "", + "Greek": "", + "Gujarati": "", + "Haitian Creole": "", + "Hausa": "", + "Hawaiian": "", + "Hebrew": "", + "Hindi": "", + "Hmong": "", + "Hungarian": "", + "Icelandic": "", + "Igbo": "", + "Indonesian": "", + "Irish": "", + "Italian": "", + "Japanese": "", + "Javanese": "", + "Kannada": "", + "Kazakh": "", + "Khmer": "", + "Korean": "", + "Kurdish": "", + "Kyrgyz": "", + "Lao": "", + "Latin": "", + "Latvian": "", + "Lithuanian": "", + "Luxembourgish": "", + "Macedonian": "", + "Malagasy": "", + "Malay": "", + "Malayalam": "", + "Maltese": "", + "Maori": "", + "Marathi": "", + "Mongolian": "", + "Nepali": "", + "Norwegian Bokmål": "", + "Nyanja": "", + "Pashto": "", + "Persian": "", + "Polish": "", + "Portuguese": "", + "Punjabi": "", + "Romanian": "", + "Russian": "", + "Samoan": "", + "Scottish Gaelic": "", + "Serbian": "", + "Shona": "", + "Sindhi": "", + "Sinhala": "", + "Slovak": "", + "Slovenian": "", + "Somali": "", + "Southern Sotho": "", + "Spanish": "", + "Spanish (Latin America)": "", + "Sundanese": "", + "Swahili": "", + "Swedish": "", + "Tajik": "", + "Tamil": "", + "Telugu": "", + "Thai": "", + "Turkish": "", + "Ukrainian": "", + "Urdu": "", + "Uzbek": "", + "Vietnamese": "", + "Welsh": "", + "Western Frisian": "", + "Xhosa": "", + "Yiddish": "", + "Yoruba": "", + "Zulu": "", + "`x` years": "`x` év", + "`x` months": "`x` hónap", + "`x` weeks": "`x` hét", + "`x` days": "`x` nap", + "`x` hours": "`x` óra", + "`x` minutes": "`x` perc", + "`x` seconds": "`x` másodperc", + "Fallback comments: ": "Másodlagos kommentek: ", + "Popular": "Népszerű", + "Top": "Top", + "About": "Leírás", + "Rating: ": "Besorolás: ", + "Language: ": "Nyelv: ", + "View as playlist": "Megtekintés playlist-ként", + "Default": "Alapértelmezett", + "Music": "Zene", + "Gaming": "Játékok", + "News": "Hírek", + "Movies": "Filmek", + "Download": "Letöltés", + "Download as: ": "Letöltés mint: ", + "%A %B %-d, %Y": "", + "(edited)": "(szerkesztve)", + "YouTube comment permalink": "YouTube komment permalink", + "permalink": "permalink", + "`x` marked it with a ❤": "`x` jelölte ❤-vel", + "Audio mode": "Audio mód", + "Video mode": "Video mód", + "Videos": "Videók", + "Playlists": "Playlistek", + "Community": "Közösség", + "Current version: ": "Jelenlegi verzió: " +} From 001ec3663e6a6ae73bac7ca2ab0f856b9029f7b0 Mon Sep 17 00:00:00 2001 From: bongo bongo Date: Sat, 29 Feb 2020 21:40:17 +0000 Subject: [PATCH 0097/2930] Add Serbian (cyrillic) translation --- locales/sr_Cyrl.json | 353 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 353 insertions(+) create mode 100644 locales/sr_Cyrl.json diff --git a/locales/sr_Cyrl.json b/locales/sr_Cyrl.json new file mode 100644 index 00000000..452ec266 --- /dev/null +++ b/locales/sr_Cyrl.json @@ -0,0 +1,353 @@ +{ + "`x` subscribers.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` subscribers.": "", + "`x` videos.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` videos.": "", + "`x` playlists.(\\D|^)1(\\D|$)": "", + "`x` playlists.": "", + "LIVE": "", + "Shared `x` ago": "", + "Unsubscribe": "", + "Subscribe": "Пратите", + "View channel on YouTube": "Погледајте канал на YouTube-у", + "View playlist on YouTube": "Погледајте плејлисту на YouTube-у", + "newest": "", + "oldest": "", + "popular": "", + "last": "", + "Next page": "", + "Previous page": "", + "Clear watch history?": "", + "New password": "", + "New passwords must match": "", + "Cannot change password for Google accounts": "", + "Authorize token?": "", + "Authorize token for `x`?": "", + "Yes": "", + "No": "", + "Import and Export Data": "", + "Import": "", + "Import Invidious data": "", + "Import YouTube subscriptions": "", + "Import FreeTube subscriptions (.db)": "", + "Import NewPipe subscriptions (.json)": "", + "Import NewPipe data (.zip)": "", + "Export": "", + "Export subscriptions as OPML": "", + "Export subscriptions as OPML (for NewPipe & FreeTube)": "", + "Export data as JSON": "", + "Delete account?": "", + "History": "", + "An alternative front-end to YouTube": "", + "JavaScript license information": "", + "source": "", + "Log in": "", + "Log in/register": "", + "Log in with Google": "", + "User ID": "", + "Password": "", + "Time (h:mm:ss):": "", + "Text CAPTCHA": "", + "Image CAPTCHA": "", + "Sign In": "", + "Register": "", + "E-mail": "", + "Google verification code": "", + "Preferences": "", + "Player preferences": "", + "Always loop: ": "", + "Autoplay: ": "", + "Play next by default: ": "", + "Autoplay next video: ": "", + "Listen by default: ": "", + "Proxy videos: ": "", + "Default speed: ": "", + "Preferred video quality: ": "", + "Player volume: ": "", + "Default comments: ": "", + "youtube": "", + "reddit": "", + "Default captions: ": "", + "Fallback captions: ": "", + "Show related videos: ": "", + "Show annotations by default: ": "", + "Visual preferences": "", + "Player style: ": "", + "Dark mode: ": "", + "Theme: ": "", + "dark": "", + "light": "", + "Thin mode: ": "", + "Subscription preferences": "", + "Show annotations by default for subscribed channels: ": "", + "Redirect homepage to feed: ": "", + "Number of videos shown in feed: ": "", + "Sort videos by: ": "", + "published": "", + "published - reverse": "", + "alphabetically": "", + "alphabetically - reverse": "", + "channel name": "", + "channel name - reverse": "", + "Only show latest video from channel: ": "", + "Only show latest unwatched video from channel: ": "", + "Only show unwatched: ": "", + "Only show notifications (if there are any): ": "", + "Enable web notifications": "", + "`x` uploaded a video": "", + "`x` is live": "", + "Data preferences": "", + "Clear watch history": "", + "Import/export data": "", + "Change password": "", + "Manage subscriptions": "", + "Manage tokens": "", + "Watch history": "", + "Delete account": "", + "Administrator preferences": "", + "Default homepage: ": "", + "Feed menu: ": "", + "Top enabled: ": "", + "CAPTCHA enabled: ": "", + "Login enabled: ": "", + "Registration enabled: ": "", + "Report statistics: ": "", + "Save preferences": "", + "Subscription manager": "", + "Token manager": "", + "Token": "", + "`x` subscriptions.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` subscriptions.": "", + "`x` tokens.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` tokens.": "", + "Import/export": "", + "unsubscribe": "", + "revoke": "", + "Subscriptions": "", + "`x` unseen notifications.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` unseen notifications.": "", + "search": "", + "Log out": "", + "Released under the AGPLv3 by Omar Roth.": "", + "Source available here.": "", + "View JavaScript license information.": "", + "View privacy policy.": "", + "Trending": "", + "Public": "", + "Unlisted": "", + "Private": "", + "View all playlists": "", + "Updated `x` ago": "", + "Delete playlist `x`?": "", + "Delete playlist": "", + "Create playlist": "", + "Title": "", + "Playlist privacy": "", + "Editing playlist `x`": "", + "Watch on YouTube": "", + "Hide annotations": "", + "Show annotations": "", + "Genre: ": "", + "License: ": "", + "Family friendly? ": "", + "Wilson score: ": "", + "Engagement: ": "", + "Whitelisted regions: ": "", + "Blacklisted regions: ": "", + "Shared `x`": "", + "`x` views.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` views.": "", + "Premieres in `x`": "", + "Premieres `x`": "", + "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "", + "View YouTube comments": "", + "View more comments on Reddit": "", + "View `x` comments.(\\D|^)1(\\D|$)": "", + "View `x` comments.": "", + "View Reddit comments": "", + "Hide replies": "", + "Show replies": "", + "Incorrect password": "", + "Quota exceeded, try again in a few hours": "", + "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "", + "Invalid TFA code": "", + "Login failed. This may be because two-factor authentication is not turned on for your account.": "", + "Wrong answer": "", + "Erroneous CAPTCHA": "", + "CAPTCHA is a required field": "", + "User ID is a required field": "", + "Password is a required field": "", + "Wrong username or password": "", + "Please sign in using 'Log in with Google'": "", + "Password cannot be empty": "", + "Password cannot be longer than 55 characters": "", + "Please log in": "", + "Invidious Private Feed for `x`": "", + "channel:`x`": "", + "Deleted or invalid channel": "", + "This channel does not exist.": "", + "Could not get channel info.": "", + "Could not fetch comments": "", + "View `x` replies.([^.,0-9]|^)1([^.,0-9]|$)": "", + "View `x` replies.": "", + "`x` ago": "", + "Load more": "", + "`x` points.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` points.": "", + "Could not create mix.": "", + "Empty playlist": "", + "Not a playlist.": "", + "Playlist does not exist.": "", + "Could not pull trending pages.": "", + "Hidden field \"challenge\" is a required field": "", + "Hidden field \"token\" is a required field": "", + "Erroneous challenge": "", + "Erroneous token": "", + "No such user": "", + "Token is expired, please try again": "", + "English": "", + "English (auto-generated)": "", + "Afrikaans": "", + "Albanian": "", + "Amharic": "", + "Arabic": "", + "Armenian": "", + "Azerbaijani": "", + "Bangla": "", + "Basque": "", + "Belarusian": "", + "Bosnian": "", + "Bulgarian": "", + "Burmese": "", + "Catalan": "", + "Cebuano": "", + "Chinese (Simplified)": "", + "Chinese (Traditional)": "", + "Corsican": "", + "Croatian": "", + "Czech": "", + "Danish": "", + "Dutch": "", + "Esperanto": "", + "Estonian": "", + "Filipino": "", + "Finnish": "", + "French": "", + "Galician": "", + "Georgian": "", + "German": "", + "Greek": "", + "Gujarati": "", + "Haitian Creole": "", + "Hausa": "", + "Hawaiian": "", + "Hebrew": "", + "Hindi": "", + "Hmong": "", + "Hungarian": "", + "Icelandic": "", + "Igbo": "", + "Indonesian": "", + "Irish": "", + "Italian": "", + "Japanese": "", + "Javanese": "", + "Kannada": "", + "Kazakh": "", + "Khmer": "", + "Korean": "", + "Kurdish": "", + "Kyrgyz": "", + "Lao": "", + "Latin": "", + "Latvian": "", + "Lithuanian": "", + "Luxembourgish": "", + "Macedonian": "", + "Malagasy": "", + "Malay": "", + "Malayalam": "", + "Maltese": "", + "Maori": "", + "Marathi": "", + "Mongolian": "", + "Nepali": "", + "Norwegian Bokmål": "", + "Nyanja": "", + "Pashto": "", + "Persian": "", + "Polish": "", + "Portuguese": "", + "Punjabi": "", + "Romanian": "", + "Russian": "", + "Samoan": "", + "Scottish Gaelic": "", + "Serbian": "", + "Shona": "", + "Sindhi": "", + "Sinhala": "", + "Slovak": "", + "Slovenian": "", + "Somali": "", + "Southern Sotho": "", + "Spanish": "", + "Spanish (Latin America)": "", + "Sundanese": "", + "Swahili": "", + "Swedish": "", + "Tajik": "", + "Tamil": "", + "Telugu": "", + "Thai": "", + "Turkish": "", + "Ukrainian": "", + "Urdu": "", + "Uzbek": "", + "Vietnamese": "", + "Welsh": "", + "Western Frisian": "", + "Xhosa": "", + "Yiddish": "", + "Yoruba": "", + "Zulu": "", + "`x` years.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` years.": "", + "`x` months.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` months.": "", + "`x` weeks.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` weeks.": "", + "`x` days.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` days.": "", + "`x` hours.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` hours.": "", + "`x` minutes.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` minutes.": "", + "`x` seconds.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` seconds.": "", + "Fallback comments: ": "", + "Popular": "", + "Top": "", + "About": "", + "Rating: ": "", + "Language: ": "", + "View as playlist": "", + "Default": "", + "Music": "", + "Gaming": "", + "News": "", + "Movies": "", + "Download": "", + "Download as: ": "", + "%A %B %-d, %Y": "", + "(edited)": "", + "YouTube comment permalink": "", + "permalink": "", + "`x` marked it with a ❤": "", + "Audio mode": "", + "Video mode": "", + "Videos": "", + "Playlists": "", + "Community": "", + "Current version: ": "Тренутна верзија: " +} From 96da04576e7c6804d82da3b55db112f3566533f4 Mon Sep 17 00:00:00 2001 From: Sylke Vicious Date: Tue, 3 Mar 2020 15:05:19 +0000 Subject: [PATCH 0098/2930] Update Italian translation --- locales/it.json | 128 ++++++++++++++++++------------------------------ 1 file changed, 49 insertions(+), 79 deletions(-) diff --git a/locales/it.json b/locales/it.json index 47510d3f..2d02d7a5 100644 --- a/locales/it.json +++ b/locales/it.json @@ -1,13 +1,9 @@ { - "`x` subscribers": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` iscritto", - "": "`x` iscritti" - }, - "`x` videos": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` video", - "": "`x` video" - }, - "`x` playlists": "", + "`x` subscribers.([^.,0-9]|^)1([^.,0-9]|$)": "`x` iscritto", + "`x` subscribers.": "`x` iscritti.", + "`x` videos.([^.,0-9]|^)1([^.,0-9]|$)": "`x` video", + "`x` videos.": "`x` video.", + "`x` playlists": "`x` playlist", "LIVE": "IN DIRETTA", "Shared `x` ago": "Condiviso `x` fa", "Unsubscribe": "Disiscriviti", @@ -75,9 +71,9 @@ "Show related videos: ": "Mostra video correlati: ", "Show annotations by default: ": "Mostra le annotazioni in modo predefinito: ", "Visual preferences": "Preferenze grafiche", - "Player style: ": "Stile riproduttore", + "Player style: ": "Stile riproduttore: ", "Dark mode: ": "Tema scuro: ", - "Theme: ": "Tema", + "Theme: ": "Tema: ", "dark": "scuro", "light": "chiaro", "Thin mode: ": "Modalità per connessioni lente: ", @@ -110,7 +106,7 @@ "Administrator preferences": "Preferenze amministratore", "Default homepage: ": "Pagina principale predefinita: ", "Feed menu: ": "Menu iscrizioni: ", - "Top enabled: ": "", + "Top enabled: ": "Top abilitato: ", "CAPTCHA enabled: ": "CAPTCHA attivati: ", "Login enabled: ": "Accesso attivato: ", "Registration enabled: ": "Registrazione attivata: ", @@ -119,40 +115,34 @@ "Subscription manager": "Gestione delle iscrizioni", "Token manager": "Gestione dei gettoni", "Token": "Gettone", - "`x` subscriptions": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` iscrizione", - "": "`x` iscrizioni" - }, - "`x` tokens": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` gettone", - "": "`x` gettoni" - }, + "`x` subscriptions.([^.,0-9]|^)1([^.,0-9]|$)": "`x` iscrizione", + "`x` subscriptions.": "`x` iscrizioni.", + "`x` tokens.([^.,0-9]|^)1([^.,0-9]|$)": "`x` gettone", + "`x` tokens.": "`x` gettoni.", "Import/export": "Importa/esporta", "unsubscribe": "disiscriviti", "revoke": "revoca", "Subscriptions": "Iscrizioni", - "`x` unseen notifications": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` notifica non visualizzata", - "": "`x` notifiche non visualizzate" - }, + "`x` unseen notifications.([^.,0-9]|^)1([^.,0-9]|$)": "`x` notifica non visualizzata", + "`x` unseen notifications.": "`x` notifiche non visualizzate.", "search": "Cerca", "Log out": "Esci", "Released under the AGPLv3 by Omar Roth.": "Pubblicato con licenza AGPLv3 da Omar Roth.", "Source available here.": "Codice sorgente.", "View JavaScript license information.": "Guarda le informazioni di licenza del codice JavaScript.", - "View privacy policy.": "Vedi la politica sulla privacy", + "View privacy policy.": "Vedi la politica sulla privacy.", "Trending": "Tendenze", - "Public": "", + "Public": "Pubblico", "Unlisted": "Non elencati", - "Private": "", - "View all playlists": "", - "Updated `x` ago": "", - "Delete playlist `x`?": "", - "Delete playlist": "", - "Create playlist": "", - "Title": "", - "Playlist privacy": "", - "Editing playlist `x`": "", + "Private": "Privato", + "View all playlists": "Visualizza tutte le playlist", + "Updated `x` ago": "Aggiornato `x` fa", + "Delete playlist `x`?": "Eliminare la playlist `x`?", + "Delete playlist": "Elimina playlist", + "Create playlist": "Crea playlist", + "Title": "Titolo", + "Playlist privacy": "Privacy playlist", + "Editing playlist `x`": "Modificando la playlist `x`", "Watch on YouTube": "Guarda su YouTube", "Hide annotations": "Nascondi annotazioni", "Show annotations": "Mostra annotazioni", @@ -164,12 +154,10 @@ "Whitelisted regions: ": "Regioni in lista bianca: ", "Blacklisted regions: ": "Regioni in lista nera: ", "Shared `x`": "Condiviso `x`", - "`x` views": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` visualizzazione", - "": "`x` visualizzazioni" - }, - "Premieres in `x`": "", - "Premieres `x`": "", + "`x` views.([^.,0-9]|^)1([^.,0-9]|$)": "`x` visualizzazione", + "`x` views.": "`x` visualizzazioni.", + "Premieres in `x`": "In anteprima in `x`", + "Premieres `x`": "In anteprima `x`", "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Ciao! Sembra che tu abbia disattivato JavaScript. Clicca qui per visualizzare i commenti. Considera che potrebbe volerci più tempo.", "View YouTube comments": "Visualizza i commenti da YouTube", "View more comments on Reddit": "Visualizza più commenti su Reddit", @@ -198,16 +186,12 @@ "This channel does not exist.": "Questo canale non esiste.", "Could not get channel info.": "Impossibile ottenere le informazioni del canale.", "Could not fetch comments": "Impossibile recuperare i commenti", - "View `x` replies": { - "([^.,0-9]|^)1([^.,0-9]|$)": "Visualizza `x` risposta", - "": "Visualizza `x` risposte" - }, + "View `x` replies.([^.,0-9]|^)1([^.,0-9]|$)": "Visualizza `x` risposta", + "View `x` replies.": "Visualizza `x` risposte.", "`x` ago": "`x` fa", "Load more": "Carica altro", - "`x` points": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` punto", - "": "`x` punti" - }, + "`x` points.([^.,0-9]|^)1([^.,0-9]|$)": "`x` punto", + "`x` points.": "`x` punti.", "Could not create mix.": "Impossibile creare il mix.", "Empty playlist": "Playlist vuota", "Not a playlist.": "Non è una playlist.", @@ -325,34 +309,20 @@ "Yiddish": "Yiddish", "Yoruba": "Yoruba", "Zulu": "Zulu", - "`x` years": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` anno", - "": "`x` anni" - }, - "`x` months": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` mese", - "": "`x` mesi" - }, - "`x` weeks": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` settimana", - "": "`x` settimane" - }, - "`x` days": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` giorno", - "": "`x` giorni" - }, - "`x` hours": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` ora", - "": "`x` ore" - }, - "`x` minutes": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` minuto", - "": "`x` minuti" - }, - "`x` seconds": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` secondo", - "": "`x` secondi" - }, + "`x` years.([^.,0-9]|^)1([^.,0-9]|$)": "`x` anno", + "`x` years.": "`x` anni.", + "`x` months.([^.,0-9]|^)1([^.,0-9]|$)": "`x` mese", + "`x` months.": "`x` mesi.", + "`x` weeks.([^.,0-9]|^)1([^.,0-9]|$)": "`x` settimana", + "`x` weeks.": "`x` settimane.", + "`x` days.([^.,0-9]|^)1([^.,0-9]|$)": "`x` giorno", + "`x` days.": "`x` giorni.", + "`x` hours.([^.,0-9]|^)1([^.,0-9]|$)": "`x` ora", + "`x` hours.": "`x` ore.", + "`x` minutes.([^.,0-9]|^)1([^.,0-9]|$)": "`x` minuto", + "`x` minutes.": "`x` minuti.", + "`x` seconds.([^.,0-9]|^)1([^.,0-9]|$)": "`x` secondo", + "`x` seconds.": "`x` secondi.", "Fallback comments: ": "Commenti alternativi: ", "Popular": "Popolare", "Top": "Top", @@ -370,7 +340,7 @@ "%A %B %-d, %Y": "%A %-d %B %Y", "(edited)": "(modificato)", "YouTube comment permalink": "Link permanente al commento di YouTube", - "permalink": "", + "permalink": "permalink", "`x` marked it with a ❤": "`x` l'ha contrassegnato con un ❤", "Audio mode": "Modalità audio", "Video mode": "Modalità video", @@ -378,4 +348,4 @@ "Playlists": "Playlist", "Community": "Comunità", "Current version: ": "Versione attuale: " -} \ No newline at end of file +} From 75fc7db50dd8a9ea5bb4706a1461a8c58c34b558 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Fri, 3 Apr 2020 16:51:19 +0000 Subject: [PATCH 0099/2930] Update Romanian translation --- locales/ro.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/ro.json b/locales/ro.json index 75496a01..9539ff35 100644 --- a/locales/ro.json +++ b/locales/ro.json @@ -326,11 +326,11 @@ "(edited)": "(editat)", "YouTube comment permalink": "Permalink pentru comentariul de pe YouTube", "permalink": "permalink", - "`x` marked it with a ❤": "`x` l-a marcat cu o ❤", + "`x` marked it with a ❤": "`x` l-a marcat cu o ❤", "Audio mode": "Mod audio", "Video mode": "Mod video", "Videos": "Videoclipuri", "Playlists": "Liste de redare", "Community": "Comunitate", "Current version: ": "Versiunea actuală: " -} \ No newline at end of file +} From fece1077f266abf922b0a7489f234b4910dd06d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Fri, 3 Apr 2020 16:51:26 +0000 Subject: [PATCH 0100/2930] Update Swedish translation --- locales/sv-SE.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/sv-SE.json b/locales/sv-SE.json index 719d0692..55e87869 100644 --- a/locales/sv-SE.json +++ b/locales/sv-SE.json @@ -64,7 +64,7 @@ "Default comments: ": "Förvalda kommentarer: ", "youtube": "YouTube", "reddit": "Reddit", - "Default captions: ": "Förvalda undertexter: ", + "Default captions: ": "Förvalda undertexter: ", "Fallback captions: ": "Ersättningsundertexter: ", "Show related videos: ": "Visa relaterade videor? ", "Show annotations by default: ": "Visa länkar-i-videon som förval? ", From 7690c6c33d9e5e60f8e52f110e7b48bc014506a9 Mon Sep 17 00:00:00 2001 From: Mihail Iosilevitch Date: Sun, 5 Apr 2020 12:41:05 +0000 Subject: [PATCH 0101/2930] Update Russian translation --- locales/ru.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/locales/ru.json b/locales/ru.json index df1dbf96..9cea15e0 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -1,7 +1,7 @@ { "`x` subscribers": "`x` подписчиков", "`x` videos": "`x` видео", - "`x` playlists": "", + "`x` playlists": "`x` плейлистов", "LIVE": "ПРЯМОЙ ЭФИР", "Shared `x` ago": "Опубликовано `x` назад", "Unsubscribe": "Отписаться", @@ -69,7 +69,7 @@ "Show related videos: ": "Показывать похожие видео? ", "Show annotations by default: ": "Всегда показывать аннотации? ", "Visual preferences": "Настройки сайта", - "Player style: ": "", + "Player style: ": "Стиль проигрывателя: ", "Dark mode: ": "Тёмное оформление: ", "Theme: ": "Тема: ", "dark": "темная", @@ -130,14 +130,14 @@ "Public": "Публичный", "Unlisted": "Нет в списке", "Private": "Приватный", - "View all playlists": "", - "Updated `x` ago": "", + "View all playlists": "Посмотреть все плейлисты", + "Updated `x` ago": "Обновлено `x` назад", "Delete playlist `x`?": "Удалить плейлист `x`?", "Delete playlist": "Удалить плейлист", "Create playlist": "Создать плейлист", - "Title": "", - "Playlist privacy": "", - "Editing playlist `x`": "", + "Title": "Заголовок", + "Playlist privacy": "Конфиденциальность плейлиста", + "Editing playlist `x`": "Редактирование плейлиста `x`", "Watch on YouTube": "Смотреть на YouTube", "Hide annotations": "Скрыть аннотации", "Show annotations": "Показать аннотации", @@ -325,12 +325,12 @@ "%A %B %-d, %Y": "%-d %B %Y, %A", "(edited)": "(изменено)", "YouTube comment permalink": "Прямая ссылка на YouTube", - "permalink": "", + "permalink": "постоянная ссылка", "`x` marked it with a ❤": "❤ от автора канала \"`x`\"", "Audio mode": "Аудио режим", "Video mode": "Видео режим", "Videos": "Видео", "Playlists": "Плейлисты", - "Community": "", + "Community": "Сообщество", "Current version: ": "Текущая версия: " } From 326f4bd681e1fa47ddc3ca37636bfc9b923a0a14 Mon Sep 17 00:00:00 2001 From: khalasa47 Date: Wed, 8 Apr 2020 17:47:49 +0000 Subject: [PATCH 0102/2930] Update Basque translation --- locales/eu.json | 80 ++++++++++++++++++++++++------------------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/locales/eu.json b/locales/eu.json index 352d84f1..eb6e877c 100644 --- a/locales/eu.json +++ b/locales/eu.json @@ -1,13 +1,13 @@ { "`x` subscribers": "`x` harpidedun", "`x` videos": "`x` bideo", - "`x` playlists": "", + "`x` playlists": "`x` erreprodukzio-zerrenda", "LIVE": "ZUZENEAN", "Shared `x` ago": "Duela `x` partekatua", "Unsubscribe": "Harpidetza kendu", "Subscribe": "Harpidetu", "View channel on YouTube": "Ikusi kanala YouTuben", - "View playlist on YouTube": "", + "View playlist on YouTube": "Ikusi erreprodukzio-zerrenda YouTuben", "newest": "berrienak", "oldest": "zaharrenak", "popular": "ospetsuenak", @@ -16,66 +16,66 @@ "Previous page": "Aurreko orria", "Clear watch history?": "Garbitu ikusitakoen historia?", "New password": "Pasahitz berria", - "New passwords must match": "", - "Cannot change password for Google accounts": "", - "Authorize token?": "", + "New passwords must match": "Pasahitza berriek bat egin behar dute", + "Cannot change password for Google accounts": "Ezin da pasahitza aldatu Google kontuetan", + "Authorize token?": "Baimendu tokena?", "Authorize token for `x`?": "", "Yes": "Bai", "No": "Ez", "Import and Export Data": "Datuak inportatu eta esportatu", "Import": "Inportatu", - "Import Invidious data": "Invidiouseko datuak inportatu", - "Import YouTube subscriptions": "YouTubeko harpidetzak inportatu", - "Import FreeTube subscriptions (.db)": "FreeTubeko harpidetzak inportatu (.db)", - "Import NewPipe subscriptions (.json)": "NewPipeko harpidetzak inportatu (.json)", - "Import NewPipe data (.zip)": "NewPipeko datuak inportatu (.zip)", + "Import Invidious data": "Inportatu Invidiouseko datuak", + "Import YouTube subscriptions": "Inportatu YouTubeko harpidetzak", + "Import FreeTube subscriptions (.db)": "Inportatu FreeTubeko harpidetzak (.db)", + "Import NewPipe subscriptions (.json)": "Inportatu NewPipeko harpidetzak (.json)", + "Import NewPipe data (.zip)": "Inportatu NewPipeko datuak (.zip)", "Export": "Esportatu", "Export subscriptions as OPML": "Esportatu harpidetzak OPML bezala", - "Export subscriptions as OPML (for NewPipe & FreeTube)": "Harpidetzak OPML bezala esportatu (NewPipe eta FreeTuberako)", - "Export data as JSON": "Datuak JSON bezala esportatu", + "Export subscriptions as OPML (for NewPipe & FreeTube)": "Esportatu harpidetzak OPML bezala (NewPipe eta FreeTuberako)", + "Export data as JSON": "Esportatu datuak JSON bezala", "Delete account?": "Kontua ezabatu?", "History": "Historia", "An alternative front-end to YouTube": "YouTuberako interfaze alternatibo bat", "JavaScript license information": "JavaScript lizentzia informazioa", "source": "iturburua", "Log in": "Saioa hasi", - "Log in/register": "Saioa hasi/Izena eman", - "Log in with Google": "Googlekin hasi saioa", + "Log in/register": "Hasi saioa / Eman izena", + "Log in with Google": "Hasi saioa Googlekin", "User ID": "Erabiltzaile IDa", "Password": "Pasahitza", - "Time (h:mm:ss):": "Denbora (o:mm:ss):", - "Text CAPTCHA": "Testu CAPTCHA", - "Image CAPTCHA": "Irudi CAPTCHA", - "Sign In": "", - "Register": "", - "E-mail": "", + "Time (h:mm:ss):": "Denbora (h:mm:ss):", + "Text CAPTCHA": "CAPTCHA testua", + "Image CAPTCHA": "CAPTCHA irudia", + "Sign In": "Hasi saioa", + "Register": "Eman izena", + "E-mail": "E-posta", "Google verification code": "", - "Preferences": "", - "Player preferences": "", + "Preferences": "Hobespenak", + "Player preferences": "Erreproduzigailuaren hobespenak", "Always loop: ": "", - "Autoplay: ": "", + "Autoplay: ": "Automatikoki erreproduzitu: ", "Play next by default: ": "", - "Autoplay next video: ": "", + "Autoplay next video: ": "Erreproduzitu automatikoki hurrengo bideoa: ", "Listen by default: ": "", "Proxy videos: ": "", "Default speed: ": "", - "Preferred video quality: ": "", - "Player volume: ": "", - "Default comments: ": "", - "youtube": "", - "reddit": "", - "Default captions: ": "", + "Preferred video quality: ": "Hobetsitako bideoaren kalitatea: ", + "Player volume: ": "Erreproduzigailuaren bolumena: ", + "Default comments: ": "Lehenetsitako iruzkinak: ", + "youtube": "youtube", + "reddit": "reddit", + "Default captions: ": "Lehenetsitako azpitituluak: ", "Fallback captions: ": "", - "Show related videos: ": "", - "Show annotations by default: ": "", - "Visual preferences": "", - "Player style: ": "", - "Dark mode: ": "", - "Theme: ": "", - "dark": "", - "light": "", + "Show related videos: ": "Erakutsi erlazionatutako bideoak: ", + "Show annotations by default: ": "Erakutsi oharrak modu lehenetsian: ", + "Visual preferences": "Hobespen bisualak", + "Player style: ": "Erreproduzigailu mota: ", + "Dark mode: ": "Gai iluna: ", + "Theme: ": "Gaia: ", + "dark": "iluna", + "light": "argia", "Thin mode: ": "", - "Subscription preferences": "", + "Subscription preferences": "Harpidetzen hobespenak", "Show annotations by default for subscribed channels: ": "", "Redirect homepage to feed: ": "", "Number of videos shown in feed: ": "", @@ -333,4 +333,4 @@ "Playlists": "", "Community": "", "Current version: ": "" -} \ No newline at end of file +} From aa30d1f359a40394a84b7a93e194020dac92d9a6 Mon Sep 17 00:00:00 2001 From: Bruno Guerreiro Date: Sun, 12 Apr 2020 13:52:35 +0000 Subject: [PATCH 0103/2930] Add Portuguese (Portugal) translation --- locales/pt_PT.json | 353 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 353 insertions(+) create mode 100644 locales/pt_PT.json diff --git a/locales/pt_PT.json b/locales/pt_PT.json new file mode 100644 index 00000000..6e144551 --- /dev/null +++ b/locales/pt_PT.json @@ -0,0 +1,353 @@ +{ + "`x` subscribers.([^.,0-9]|^)1([^.,0-9]|$)": "`x` subscritores.([^.,0-9]|^)1([^.,0-9]|$)", + "`x` subscribers.": "`x` subscritores.", + "`x` videos.([^.,0-9]|^)1([^.,0-9]|$)": "`x` vídeos.([^.,0-9]|^)1([^.,0-9]|$)", + "`x` videos.": "`x` vídeos.", + "`x` playlists.(\\D|^)1(\\D|$)": "`x` listas de reprodução.(\\D|^)1(\\D|$)", + "`x` playlists.": "`x` listas de reprodução.", + "LIVE": "Em direto", + "Shared `x` ago": "Partilhado `x` atrás", + "Unsubscribe": "Anular subscrição", + "Subscribe": "Subscrever", + "View channel on YouTube": "Ver canal no YouTube", + "View playlist on YouTube": "Ver lista de reprodução no YouTube", + "newest": "mais recentes", + "oldest": "mais antigos", + "popular": "popular", + "last": "últimos", + "Next page": "Próxima página", + "Previous page": "Página anterior", + "Clear watch history?": "Limpar histórico de reprodução?", + "New password": "Nova palavra-chave", + "New passwords must match": "As novas palavra-chaves devem corresponder", + "Cannot change password for Google accounts": "Não é possível alterar palavra-chave para contas do Google", + "Authorize token?": "Autorizar token?", + "Authorize token for `x`?": "Autorizar token para `x`?", + "Yes": "Sim", + "No": "Não", + "Import and Export Data": "Importar e Exportar Dados", + "Import": "Importar", + "Import Invidious data": "Importar dados do Invidious", + "Import YouTube subscriptions": "Importar subscrições do YouTube", + "Import FreeTube subscriptions (.db)": "Importar subscrições do FreeTube (.db)", + "Import NewPipe subscriptions (.json)": "Importar subscrições do NewPipe (.json)", + "Import NewPipe data (.zip)": "Importar dados do NewPipe (.zip)", + "Export": "Exportar", + "Export subscriptions as OPML": "Exportar subscrições como OPML", + "Export subscriptions as OPML (for NewPipe & FreeTube)": "Exportar subscrições como OPML (para NewPipe e FreeTube)", + "Export data as JSON": "Exportar dados como JSON", + "Delete account?": "Eliminar conta?", + "History": "Histórico", + "An alternative front-end to YouTube": "Uma interface alternativa para o YouTube", + "JavaScript license information": "Informação de licença do JavaScript", + "source": "código-fonte", + "Log in": "Iniciar sessão", + "Log in/register": "Iniciar sessão/Registar", + "Log in with Google": "Iniciar sessão com o Google", + "User ID": "Utilizador", + "Password": "Palavra-chave", + "Time (h:mm:ss):": "Tempo (h:mm:ss):", + "Text CAPTCHA": "Texto CAPTCHA", + "Image CAPTCHA": "Imagem CAPTCHA", + "Sign In": "Iniciar Sessão", + "Register": "Registar", + "E-mail": "E-mail", + "Google verification code": "Código de verificação do Google", + "Preferences": "Preferências", + "Player preferences": "Preferências do reprodutor", + "Always loop: ": "Repetir sempre: ", + "Autoplay: ": "Reprodução automática: ", + "Play next by default: ": "Sempre reproduzir próximo: ", + "Autoplay next video: ": "Reproduzir próximo vídeo automaticamente: ", + "Listen by default: ": "Apenas áudio: ", + "Proxy videos: ": "Usar proxy nos vídeos: ", + "Default speed: ": "Velocidade preferida: ", + "Preferred video quality: ": "Qualidade de vídeo preferida: ", + "Player volume: ": "Volume da reprodução: ", + "Default comments: ": "Preferência dos comentários: ", + "youtube": "youtube", + "reddit": "reddit", + "Default captions: ": "Legendas predefinidas: ", + "Fallback captions: ": "Legendas alternativas: ", + "Show related videos: ": "Mostrar vídeos relacionados: ", + "Show annotations by default: ": "Mostrar sempre anotações: ", + "Visual preferences": "Preferências visuais", + "Player style: ": "Estilo do reprodutor: ", + "Dark mode: ": "Modo escuro: ", + "Theme: ": "Tema: ", + "dark": "escuro", + "light": "claro", + "Thin mode: ": "Modo compacto: ", + "Subscription preferences": "Preferências de subscrições", + "Show annotations by default for subscribed channels: ": "Mostrar sempre anotações para os canais subscritos: ", + "Redirect homepage to feed: ": "Redirecionar página inicial para subscrições: ", + "Number of videos shown in feed: ": "Número de vídeos nas subscrições: ", + "Sort videos by: ": "Ordenar vídeos por: ", + "published": "publicado", + "published - reverse": "publicado - inverso", + "alphabetically": "alfabeticamente", + "alphabetically - reverse": "alfabeticamente - inverso", + "channel name": "nome do canal", + "channel name - reverse": "nome do canal - inverso", + "Only show latest video from channel: ": "Mostrar apenas o vídeo mais recente do canal: ", + "Only show latest unwatched video from channel: ": "Mostrar apenas vídeos mais recentes não visualizados do canal: ", + "Only show unwatched: ": "Mostrar apenas vídeos não visualizados: ", + "Only show notifications (if there are any): ": "Mostrar apenas notificações (se existirem): ", + "Enable web notifications": "Ativar notificações pela web", + "`x` uploaded a video": "`x` publicou um novo vídeo", + "`x` is live": "`x` está em direto", + "Data preferences": "Preferências de dados", + "Clear watch history": "Limpar histórico de reprodução", + "Import/export data": "Importar/Exportar dados", + "Change password": "Alterar palavra-chave", + "Manage subscriptions": "Gerir as subscrições", + "Manage tokens": "Gerir tokens", + "Watch history": "Histórico de reprodução", + "Delete account": "Eliminar conta", + "Administrator preferences": "Preferências de administrador", + "Default homepage: ": "Página inicial padrão: ", + "Feed menu: ": "Menu de subscrições: ", + "Top enabled: ": "Top ativado: ", + "CAPTCHA enabled: ": "CAPTCHA ativado: ", + "Login enabled: ": "Iniciar sessão ativado: ", + "Registration enabled: ": "Registar ativado: ", + "Report statistics: ": "Relatório de estatísticas: ", + "Save preferences": "Gravar preferências", + "Subscription manager": "Gerir subscrições", + "Token manager": "Gerir tokens", + "Token": "Token", + "`x` subscriptions.([^.,0-9]|^)1([^.,0-9]|$)": "`x` subscrições.([^.,0-9]|^)1([^.,0-9]|$)", + "`x` subscriptions.": "`x` subscrições.", + "`x` tokens.([^.,0-9]|^)1([^.,0-9]|$)": "`x` tokens.([^.,0-9]|^)1([^.,0-9]|$)", + "`x` tokens.": "`x` tokens.", + "Import/export": "Importar/Exportar", + "unsubscribe": "Anular subscrição", + "revoke": "revogar", + "Subscriptions": "Subscrições", + "`x` unseen notifications.([^.,0-9]|^)1([^.,0-9]|$)": "`x` notificações não vistas.([^.,0-9]|^)1([^.,0-9]|$)", + "`x` unseen notifications.": "\"x\" notificações não vistas.", + "search": "Pesquisar", + "Log out": "Terminar sessão", + "Released under the AGPLv3 by Omar Roth.": "Publicado sob a licença AGPLv3, por Omar Roth.", + "Source available here.": "Código-fonte disponível aqui.", + "View JavaScript license information.": "Ver informações da licença do JavaScript.", + "View privacy policy.": "Ver a política de privacidade.", + "Trending": "Tendências", + "Public": "Público", + "Unlisted": "Não listado", + "Private": "Privado", + "View all playlists": "Ver todas as listas de reprodução", + "Updated `x` ago": "Atualizado `x` atrás", + "Delete playlist `x`?": "Eliminar a lista de reprodução 'x'?", + "Delete playlist": "Eliminar lista de reprodução", + "Create playlist": "Criar lista de reprodução", + "Title": "Título", + "Playlist privacy": "Privacidade da lista de reprodução", + "Editing playlist `x`": "A editar lista de reprodução 'x'", + "Watch on YouTube": "Ver no YouTube", + "Hide annotations": "Ocultar anotações", + "Show annotations": "Mostrar anotações", + "Genre: ": "Género: ", + "License: ": "Licença: ", + "Family friendly? ": "Filtrar conteúdo impróprio: ", + "Wilson score: ": "Pontuação de Wilson: ", + "Engagement: ": "Compromisso: ", + "Whitelisted regions: ": "Regiões permitidas: ", + "Blacklisted regions: ": "Regiões bloqueadas: ", + "Shared `x`": "Partilhado `x`", + "`x` views.([^.,0-9]|^)1([^.,0-9]|$)": "`x` visualizações.([^.,0-9]|^)1([^.,0-9]|$)", + "`x` views.": "`x` visualizações.", + "Premieres in `x`": "Estreias em 'x'", + "Premieres `x`": "Estreias 'x'", + "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Oi! Parece que JavaScript está desativado. Clique aqui para ver os comentários, entretanto eles podem levar mais tempo para carregar.", + "View YouTube comments": "Ver comentários do YouTube", + "View more comments on Reddit": "Ver mais comentários no Reddit", + "View `x` comments.(\\D|^)1(\\D|$)": "Ver `x` comentários.(\\D|^)1(\\D|$)", + "View `x` comments.": "Ver `x` comentários.", + "View Reddit comments": "Ver comentários do Reddit", + "Hide replies": "Ocultar respostas", + "Show replies": "Mostrar respostas", + "Incorrect password": "Palavra-chave incorreta", + "Quota exceeded, try again in a few hours": "Cota excedida. Tente novamente dentro de algumas horas", + "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Não é possível iniciar sessão, certifique-se de que a autenticação de dois fatores (Autenticador ou SMS) está ativada.", + "Invalid TFA code": "Código TFA inválido", + "Login failed. This may be because two-factor authentication is not turned on for your account.": "Falhou o início de sessão. Isto pode ser devido a dois fatores de autenticação não está ativado para sua conta.", + "Wrong answer": "Resposta errada", + "Erroneous CAPTCHA": "CAPTCHA inválido", + "CAPTCHA is a required field": "CAPTCHA é um campo obrigatório", + "User ID is a required field": "O nome de utilizador é um campo obrigatório", + "Password is a required field": "Palavra-chave é um campo obrigatório", + "Wrong username or password": "Nome de utilizador ou palavra-chave incorreto", + "Please sign in using 'Log in with Google'": "Por favor, inicie sessão usando 'Iniciar sessão com o Google'", + "Password cannot be empty": "A palavra-chave não pode estar vazia", + "Password cannot be longer than 55 characters": "A palavra-chave não pode ser superior a 55 caracteres", + "Please log in": "Por favor, inicie sessão", + "Invidious Private Feed for `x`": "Feed Privado do Invidious para `x`", + "channel:`x`": "canal:'x'", + "Deleted or invalid channel": "Canal apagado ou inválido", + "This channel does not exist.": "Este canal não existe.", + "Could not get channel info.": "Não foi possível obter as informações do canal.", + "Could not fetch comments": "Não foi possível obter os comentários", + "View `x` replies.([^.,0-9]|^)1([^.,0-9]|$)": "Ver `x` respostas.([^.,0-9]|^)1([^.,0-9]|$)", + "View `x` replies.": "Ver `x` respostas.", + "`x` ago": "`x` atrás", + "Load more": "Carregar mais", + "`x` points.([^.,0-9]|^)1([^.,0-9]|$)": "'x' pontos. ([^.,0-9]^)1(^.0-9][[$]", + "`x` points.": "'x' pontos.", + "Could not create mix.": "Não foi possível criar mistura.", + "Empty playlist": "Lista de reprodução vazia", + "Not a playlist.": "Não é uma lista de reprodução.", + "Playlist does not exist.": "A lista de reprodução não existe.", + "Could not pull trending pages.": "Não foi possível obter páginas de tendências.", + "Hidden field \"challenge\" is a required field": "O campo oculto \"desafio\" é obrigatório", + "Hidden field \"token\" is a required field": "O campo oculto \"token\" é um campo obrigatório", + "Erroneous challenge": "Desafio inválido", + "Erroneous token": "Token inválido", + "No such user": "Utilizador inválido", + "Token is expired, please try again": "Token expirou, tente novamente", + "English": "Inglês", + "English (auto-generated)": "Inglês (auto-gerado)", + "Afrikaans": "Africano", + "Albanian": "Albanês", + "Amharic": "Amárico", + "Arabic": "Árabe", + "Armenian": "Arménio", + "Azerbaijani": "Azerbaijano", + "Bangla": "Bangla", + "Basque": "Basco", + "Belarusian": "Bielorrusso", + "Bosnian": "Bósnio", + "Bulgarian": "Búlgaro", + "Burmese": "Birmanês", + "Catalan": "Catalão", + "Cebuano": "Cebuano", + "Chinese (Simplified)": "Chinês (Simplificado)", + "Chinese (Traditional)": "Chinês (Tradicional)", + "Corsican": "Corso", + "Croatian": "Croata", + "Czech": "Checo", + "Danish": "Dinamarquês", + "Dutch": "Holandês", + "Esperanto": "Esperanto", + "Estonian": "Estónio", + "Filipino": "Filipino", + "Finnish": "Finlandês", + "French": "Francês", + "Galician": "Galego", + "Georgian": "Georgiano", + "German": "Alemão", + "Greek": "Grego", + "Gujarati": "Guzerate", + "Haitian Creole": "Crioulo haitiano", + "Hausa": "Hauçá", + "Hawaiian": "Havaiano", + "Hebrew": "Hebraico", + "Hindi": "Hindi", + "Hmong": "Hmong", + "Hungarian": "Húngaro", + "Icelandic": "Islandês", + "Igbo": "Igbo", + "Indonesian": "Indonésio", + "Irish": "Irlandês", + "Italian": "Italiano", + "Japanese": "Japonês", + "Javanese": "Javanês", + "Kannada": "Canarim", + "Kazakh": "Cazaque", + "Khmer": "Khmer", + "Korean": "Coreano", + "Kurdish": "Curdo", + "Kyrgyz": "Quirguiz", + "Lao": "Laosiano", + "Latin": "Latim", + "Latvian": "Letão", + "Lithuanian": "Lituano", + "Luxembourgish": "Luxemburguês", + "Macedonian": "Macedónio", + "Malagasy": "Malgaxe", + "Malay": "Malaio", + "Malayalam": "Malaiala", + "Maltese": "Maltês", + "Maori": "Maori", + "Marathi": "Marathi", + "Mongolian": "Mongol", + "Nepali": "Nepalês", + "Norwegian Bokmål": "Bokmål norueguês", + "Nyanja": "Nyanja", + "Pashto": "Pashto", + "Persian": "Persa", + "Polish": "Polaco", + "Portuguese": "Português", + "Punjabi": "Punjabi", + "Romanian": "Romeno", + "Russian": "Russo", + "Samoan": "Samoano", + "Scottish Gaelic": "Gaélico escocês", + "Serbian": "Sérvio", + "Shona": "Shona", + "Sindhi": "Sindhi", + "Sinhala": "Cingalês", + "Slovak": "Eslovaco", + "Slovenian": "Esloveno", + "Somali": "Somali", + "Southern Sotho": "Sotho do Sul", + "Spanish": "Espanhol", + "Spanish (Latin America)": "Espanhol (América Latina)", + "Sundanese": "Sudanês", + "Swahili": "Suaíli", + "Swedish": "Sueco", + "Tajik": "Tajique", + "Tamil": "Tâmil", + "Telugu": "Telugu", + "Thai": "Tailandês", + "Turkish": "Turco", + "Ukrainian": "Ucraniano", + "Urdu": "Urdu", + "Uzbek": "Uzbeque", + "Vietnamese": "Vietnamita", + "Welsh": "Galês", + "Western Frisian": "Frísio Ocidental", + "Xhosa": "Xhosa", + "Yiddish": "Iídiche", + "Yoruba": "Ioruba", + "Zulu": "Zulu", + "`x` years.([^.,0-9]|^)1([^.,0-9]|$)": "`x` anos.([^.,0-9]|^)1([^.,0-9]|$)", + "`x` years.": "`x` anos.", + "`x` months.([^.,0-9]|^)1([^.,0-9]|$)": "`x` meses.([^.,0-9]|^)1([^.,0-9]|$)", + "`x` months.": "`x` meses.", + "`x` weeks.([^.,0-9]|^)1([^.,0-9]|$)": "`x` semanas.([^.,0-9]|^)1([^.,0-9]|$)", + "`x` weeks.": "`x` semanas.", + "`x` days.([^.,0-9]|^)1([^.,0-9]|$)": "`x` dias.([^.,0-9]|^)1([^.,0-9]|$)", + "`x` days.": "`x` dias.", + "`x` hours.([^.,0-9]|^)1([^.,0-9]|$)": "`x` horas.([^.,0-9]|^)1([^.,0-9]|$)", + "`x` hours.": "`x` horas.", + "`x` minutes.([^.,0-9]|^)1([^.,0-9]|$)": "`x` minutos.([^.,0-9]|^)1([^.,0-9]|$)", + "`x` minutes.": "`x` minutos.", + "`x` seconds.([^.,0-9]|^)1([^.,0-9]|$)": "`x` segundos.([^.,0-9]|^)1([^.,0-9]|$)", + "`x` seconds.": "`x` segundos.", + "Fallback comments: ": "Comentários alternativos: ", + "Popular": "Popular", + "Top": "Top", + "About": "Sobre", + "Rating: ": "Avaliação: ", + "Language: ": "Idioma: ", + "View as playlist": "Ver como lista de reprodução", + "Default": "Predefinição", + "Music": "Música", + "Gaming": "Jogos", + "News": "Notícias", + "Movies": "Filmes", + "Download": "Transferir", + "Download as: ": "Transferir como: ", + "%A %B %-d, %Y": "%A %B %-d, %Y", + "(edited)": "(editado)", + "YouTube comment permalink": "Link permanente do comentário do YouTube", + "permalink": "ligação permanente", + "`x` marked it with a ❤": "`x` foi marcado como ❤", + "Audio mode": "Modo de áudio", + "Video mode": "Modo de vídeo", + "Videos": "Vídeos", + "Playlists": "Listas de reprodução", + "Community": "Comunidade", + "Current version: ": "Versão atual: " +} From 8e1791570ee95d3b23faaf2214cc8cb6337fc645 Mon Sep 17 00:00:00 2001 From: Tolstovka Date: Sat, 11 Apr 2020 16:47:41 +0000 Subject: [PATCH 0104/2930] Update Ukrainian translation --- locales/uk.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/uk.json b/locales/uk.json index 5679949f..e2e4a63c 100644 --- a/locales/uk.json +++ b/locales/uk.json @@ -325,12 +325,12 @@ "%A %B %-d, %Y": "%-d %B %Y, %A", "(edited)": "(змінено)", "YouTube comment permalink": "Пряме посилання на коментар в YouTube", - "permalink": "", + "permalink": "постійне посилання", "`x` marked it with a ❤": "❤ цьому від каналу `x`", "Audio mode": "Аудіорежим", "Video mode": "Відеорежим", "Videos": "Відео", "Playlists": "Плейлисти", - "Community": "", + "Community": "Спільнота", "Current version: ": "Поточна версія: " } From 61c8256ef0eea8abbcdb53118f7d17a7a9001a9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?O=C4=9Fuz=20Ersen?= Date: Sun, 12 Apr 2020 21:34:30 +0000 Subject: [PATCH 0105/2930] Update Turkish translation --- locales/tr.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/locales/tr.json b/locales/tr.json index ed18f393..b2794f65 100644 --- a/locales/tr.json +++ b/locales/tr.json @@ -56,20 +56,20 @@ "Player preferences": "Oynatıcı tercihleri", "Always loop: ": "Sürekli döngü: ", "Autoplay: ": "Otomatik oynat: ", - "Play next by default: ": "Varsayılan olarak sonrakini oynat: ", + "Play next by default: ": "Öntanımlı olarak sonrakini oynat: ", "Autoplay next video: ": "Sonraki videoyu otomatik oynat: ", - "Listen by default: ": "Varsayılan olarak dinle: ", + "Listen by default: ": "Öntanımlı olarak dinle: ", "Proxy videos: ": "Videoları proxy'le: ", - "Default speed: ": "Varsayılan hız: ", + "Default speed: ": "Öntanımlı hız: ", "Preferred video quality: ": "Tercih edilen video kalitesi: ", "Player volume: ": "Oynatıcı ses seviyesi: ", - "Default comments: ": "Varsayılan yorumlar: ", + "Default comments: ": "Öntanımlı yorumlar: ", "youtube": "youtube", "reddit": "reddit", - "Default captions: ": "Varsayılan altyazılar: ", + "Default captions: ": "Öntanımlı altyazılar: ", "Fallback captions: ": "Yedek altyazılar: ", "Show related videos: ": "İlgili videoları göster: ", - "Show annotations by default: ": "Varsayılan olarak ek açıklamaları göster: ", + "Show annotations by default: ": "Öntanımlı olarak ek açıklamaları göster: ", "Visual preferences": "Görsel tercihler", "Player style: ": "Oynatıcı biçimi: ", "Dark mode: ": "Karanlık mod: ", @@ -78,7 +78,7 @@ "light": "aydınlık", "Thin mode: ": "İnce mod: ", "Subscription preferences": "Abonelik tercihleri", - "Show annotations by default for subscribed channels: ": "Abone olunan kanallar için ek açıklamaları varsayılan olarak göster: ", + "Show annotations by default for subscribed channels: ": "Abone olunan kanallar için ek açıklamaları öntanımlı olarak göster: ", "Redirect homepage to feed: ": "Ana sayfayı akışa yönlendir: ", "Number of videos shown in feed: ": "Akışta gösterilen video sayısı: ", "Sort videos by: ": "Videoları sıralama kriteri: ", @@ -104,7 +104,7 @@ "Watch history": "İzleme geçmişi", "Delete account": "Hesap silme", "Administrator preferences": "Yönetici tercihleri", - "Default homepage: ": "Varsayılan ana sayfa: ", + "Default homepage: ": "Öntanımlı ana sayfa: ", "Feed menu: ": "Akış menüsü: ", "Top enabled: ": "Top etkin: ", "CAPTCHA enabled: ": "CAPTCHA etkin: ", @@ -138,7 +138,7 @@ "Title": "Başlık", "Playlist privacy": "Çalma listesi gizliliği", "Editing playlist `x`": "`x` çalma listesi düzenleniyor", - "Source available here.": "Kaynak kodu burada mevcut.", + "Source available here.": "Kaynak kodları burada bulunabilir.", "View JavaScript license information.": "JavaScript lisans bilgilerini görüntüle.", "View privacy policy.": "Gizlilik politikasını görüntüle.", "Trending": "Trendler", @@ -323,7 +323,7 @@ "Rating: ": "Değerlendirme: ", "Language: ": "Dil: ", "View as playlist": "Oynatma listesi olarak görüntüle", - "Default": "Varsayılan", + "Default": "Öntanımlı", "Music": "Müzik", "Gaming": "Oyun", "News": "Haberler", @@ -340,5 +340,5 @@ "Videos": "Videolar", "Playlists": "Oynatma listeleri", "Community": "Topluluk", - "Current version: ": "Şu anki versiyon: " + "Current version: ": "Şu anki sürüm: " } From 9a2a636aed3c003b9963a509bb4d94ded5c53183 Mon Sep 17 00:00:00 2001 From: Lucas Araujo Date: Mon, 20 Apr 2020 19:35:05 +0000 Subject: [PATCH 0106/2930] Update Portuguese (Brazil) translation --- locales/pt-BR.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/locales/pt-BR.json b/locales/pt-BR.json index 5a4bcfc8..c3fa5a1e 100644 --- a/locales/pt-BR.json +++ b/locales/pt-BR.json @@ -1,7 +1,7 @@ { "`x` subscribers": "`x` inscritos", "`x` videos": "`x` videos", - "`x` playlists": "", + "`x` playlists": "`x` lista de reprodução", "LIVE": "AO VIVO", "Shared `x` ago": "Compartilhado `x` atrás", "Unsubscribe": "Desinscrever-se", @@ -325,12 +325,12 @@ "%A %B %-d, %Y": "%A %-d %B %Y", "(edited)": "(editado)", "YouTube comment permalink": "Link permanente do comentário do YouTube", - "permalink": "", + "permalink": "Link permanente", "`x` marked it with a ❤": "`x` foi marcado como ❤", "Audio mode": "Modo de audio", "Video mode": "Modo de video", - "Videos": "Videos", + "Videos": "Vídeos", "Playlists": "Listas de reprodução", "Community": "Comunidade", "Current version: ": "Versão atual: " -} \ No newline at end of file +} From 97eb01a28d007cac7af86779f26c92397b68b4c8 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Mon, 20 Apr 2020 16:40:03 -0500 Subject: [PATCH 0107/2930] Merge weblate --- locales/ar.json | 2 +- locales/de.json | 2 +- locales/en-US.json | 4 +- locales/eo.json | 2 +- locales/es.json | 2 +- locales/eu.json | 2 +- locales/fr.json | 2 +- locales/hu-HU.json | 2 +- locales/it.json | 92 ++++++++++++++++--------- locales/ja.json | 6 +- locales/nb-NO.json | 2 +- locales/nl.json | 2 +- locales/pl.json | 2 +- locales/pt-BR.json | 2 +- locales/{pt_PT.json => pt-PT.json} | 104 +++++++++++++++++++---------- locales/ro.json | 2 +- locales/ru.json | 2 +- locales/sr_Cyrl.json | 19 +----- locales/sv-SE.json | 2 +- locales/tr.json | 2 +- locales/uk.json | 2 +- locales/zh-CN.json | 2 +- src/invidious.cr | 2 + 23 files changed, 155 insertions(+), 106 deletions(-) rename locales/{pt_PT.json => pt-PT.json} (86%) diff --git a/locales/ar.json b/locales/ar.json index c580a2d5..12bcc199 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -333,4 +333,4 @@ "Playlists": "قوائم التشغيل", "Community": "المجتمع", "Current version: ": "الإصدار الحالي: " -} +} \ No newline at end of file diff --git a/locales/de.json b/locales/de.json index acf82e8b..b685a842 100644 --- a/locales/de.json +++ b/locales/de.json @@ -333,4 +333,4 @@ "Playlists": "Wiedergabelisten", "Community": "Gemeinschaft", "Current version: ": "Aktuelle Version: " -} +} \ No newline at end of file diff --git a/locales/en-US.json b/locales/en-US.json index b61515c9..acd2b667 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -8,7 +8,7 @@ "": "`x` videos" }, "`x` playlists": { - "(\\D|^)1(\\D|$)": "`x` playlist", + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` playlist", "": "`x` playlists" }, "LIVE": "LIVE", @@ -177,7 +177,7 @@ "View YouTube comments": "View YouTube comments", "View more comments on Reddit": "View more comments on Reddit", "View `x` comments": { - "(\\D|^)1(\\D|$)": "View `x` comment", + "([^.,0-9]|^)1([^.,0-9]|$)": "View `x` comment", "": "View `x` comments" }, "View Reddit comments": "View Reddit comments", diff --git a/locales/eo.json b/locales/eo.json index a42f0330..ae640e37 100644 --- a/locales/eo.json +++ b/locales/eo.json @@ -333,4 +333,4 @@ "Playlists": "Ludlistoj", "Community": "Komunumo", "Current version: ": "Nuna versio: " -} +} \ No newline at end of file diff --git a/locales/es.json b/locales/es.json index 08acba92..7fc75003 100644 --- a/locales/es.json +++ b/locales/es.json @@ -333,4 +333,4 @@ "Playlists": "Listas de reproducción", "Community": "Comunidad", "Current version: ": "Versión actual: " -} +} \ No newline at end of file diff --git a/locales/eu.json b/locales/eu.json index eb6e877c..61299c72 100644 --- a/locales/eu.json +++ b/locales/eu.json @@ -333,4 +333,4 @@ "Playlists": "", "Community": "", "Current version: ": "" -} +} \ No newline at end of file diff --git a/locales/fr.json b/locales/fr.json index fa82c4c5..24cabdea 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -333,4 +333,4 @@ "Playlists": "Listes de lecture", "Community": "Communauté", "Current version: ": "Version actuelle : " -} +} \ No newline at end of file diff --git a/locales/hu-HU.json b/locales/hu-HU.json index 29a35da8..b21ae93a 100644 --- a/locales/hu-HU.json +++ b/locales/hu-HU.json @@ -332,4 +332,4 @@ "Playlists": "Playlistek", "Community": "Közösség", "Current version: ": "Jelenlegi verzió: " -} +} \ No newline at end of file diff --git a/locales/it.json b/locales/it.json index 2d02d7a5..2e993c81 100644 --- a/locales/it.json +++ b/locales/it.json @@ -1,8 +1,12 @@ { - "`x` subscribers.([^.,0-9]|^)1([^.,0-9]|$)": "`x` iscritto", - "`x` subscribers.": "`x` iscritti.", - "`x` videos.([^.,0-9]|^)1([^.,0-9]|$)": "`x` video", - "`x` videos.": "`x` video.", + "`x` subscribers.": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` iscritto", + "": "`x` iscritti." + }, + "`x` videos.": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` video", + "": "`x` video." + }, "`x` playlists": "`x` playlist", "LIVE": "IN DIRETTA", "Shared `x` ago": "Condiviso `x` fa", @@ -115,16 +119,22 @@ "Subscription manager": "Gestione delle iscrizioni", "Token manager": "Gestione dei gettoni", "Token": "Gettone", - "`x` subscriptions.([^.,0-9]|^)1([^.,0-9]|$)": "`x` iscrizione", - "`x` subscriptions.": "`x` iscrizioni.", - "`x` tokens.([^.,0-9]|^)1([^.,0-9]|$)": "`x` gettone", - "`x` tokens.": "`x` gettoni.", + "`x` subscriptions.": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` iscrizione", + "": "`x` iscrizioni." + }, + "`x` tokens.": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` gettone", + "": "`x` gettoni." + }, "Import/export": "Importa/esporta", "unsubscribe": "disiscriviti", "revoke": "revoca", "Subscriptions": "Iscrizioni", - "`x` unseen notifications.([^.,0-9]|^)1([^.,0-9]|$)": "`x` notifica non visualizzata", - "`x` unseen notifications.": "`x` notifiche non visualizzate.", + "`x` unseen notifications.": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` notifica non visualizzata", + "": "`x` notifiche non visualizzate." + }, "search": "Cerca", "Log out": "Esci", "Released under the AGPLv3 by Omar Roth.": "Pubblicato con licenza AGPLv3 da Omar Roth.", @@ -154,8 +164,10 @@ "Whitelisted regions: ": "Regioni in lista bianca: ", "Blacklisted regions: ": "Regioni in lista nera: ", "Shared `x`": "Condiviso `x`", - "`x` views.([^.,0-9]|^)1([^.,0-9]|$)": "`x` visualizzazione", - "`x` views.": "`x` visualizzazioni.", + "`x` views.": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` visualizzazione", + "": "`x` visualizzazioni." + }, "Premieres in `x`": "In anteprima in `x`", "Premieres `x`": "In anteprima `x`", "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Ciao! Sembra che tu abbia disattivato JavaScript. Clicca qui per visualizzare i commenti. Considera che potrebbe volerci più tempo.", @@ -186,12 +198,16 @@ "This channel does not exist.": "Questo canale non esiste.", "Could not get channel info.": "Impossibile ottenere le informazioni del canale.", "Could not fetch comments": "Impossibile recuperare i commenti", - "View `x` replies.([^.,0-9]|^)1([^.,0-9]|$)": "Visualizza `x` risposta", - "View `x` replies.": "Visualizza `x` risposte.", + "View `x` replies.": { + "([^.,0-9]|^)1([^.,0-9]|$)": "Visualizza `x` risposta", + "": "Visualizza `x` risposte." + }, "`x` ago": "`x` fa", "Load more": "Carica altro", - "`x` points.([^.,0-9]|^)1([^.,0-9]|$)": "`x` punto", - "`x` points.": "`x` punti.", + "`x` points.": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` punto", + "": "`x` punti." + }, "Could not create mix.": "Impossibile creare il mix.", "Empty playlist": "Playlist vuota", "Not a playlist.": "Non è una playlist.", @@ -309,20 +325,34 @@ "Yiddish": "Yiddish", "Yoruba": "Yoruba", "Zulu": "Zulu", - "`x` years.([^.,0-9]|^)1([^.,0-9]|$)": "`x` anno", - "`x` years.": "`x` anni.", - "`x` months.([^.,0-9]|^)1([^.,0-9]|$)": "`x` mese", - "`x` months.": "`x` mesi.", - "`x` weeks.([^.,0-9]|^)1([^.,0-9]|$)": "`x` settimana", - "`x` weeks.": "`x` settimane.", - "`x` days.([^.,0-9]|^)1([^.,0-9]|$)": "`x` giorno", - "`x` days.": "`x` giorni.", - "`x` hours.([^.,0-9]|^)1([^.,0-9]|$)": "`x` ora", - "`x` hours.": "`x` ore.", - "`x` minutes.([^.,0-9]|^)1([^.,0-9]|$)": "`x` minuto", - "`x` minutes.": "`x` minuti.", - "`x` seconds.([^.,0-9]|^)1([^.,0-9]|$)": "`x` secondo", - "`x` seconds.": "`x` secondi.", + "`x` years.": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` anno", + "": "`x` anni." + }, + "`x` months.": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` mese", + "": "`x` mesi." + }, + "`x` weeks.": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` settimana", + "": "`x` settimane." + }, + "`x` days.": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` giorno", + "": "`x` giorni." + }, + "`x` hours.": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` ora", + "": "`x` ore." + }, + "`x` minutes.": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` minuto", + "": "`x` minuti." + }, + "`x` seconds.": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` secondo", + "": "`x` secondi." + }, "Fallback comments: ": "Commenti alternativi: ", "Popular": "Popolare", "Top": "Top", @@ -348,4 +378,4 @@ "Playlists": "Playlist", "Community": "Comunità", "Current version: ": "Versione attuale: " -} +} \ No newline at end of file diff --git a/locales/ja.json b/locales/ja.json index e2aabd0b..e9ca0e62 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -8,7 +8,7 @@ "": "`x` 個の動画" }, "`x` playlists": { - "(\\D|^)1(\\D|$)": "`x` 個の再生リスト", + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` 個の再生リスト", "": "`x` 個の再生リスト" }, "LIVE": "ライブ", @@ -177,7 +177,7 @@ "View YouTube comments": "YouTube のコメントを見る", "View more comments on Reddit": "Reddit でコメントをもっと見る", "View `x` comments": { - "(\\D|^)1(\\D|$)": "`x` 件のコメントを見る", + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` 件のコメントを見る", "": "`x` 件のコメントを見る" }, "View Reddit comments": "Reddit のコメントを見る", @@ -384,4 +384,4 @@ "Playlists": "プレイリスト", "Community": "コミュニティ", "Current version: ": "現在のバージョン: " -} +} \ No newline at end of file diff --git a/locales/nb-NO.json b/locales/nb-NO.json index cda55a1d..ff40e27b 100644 --- a/locales/nb-NO.json +++ b/locales/nb-NO.json @@ -333,4 +333,4 @@ "Playlists": "Spillelister", "Community": "Gemenskap", "Current version: ": "Nåværende versjon: " -} +} \ No newline at end of file diff --git a/locales/nl.json b/locales/nl.json index b2221efb..29af954a 100644 --- a/locales/nl.json +++ b/locales/nl.json @@ -334,4 +334,4 @@ "Community": "Gemeenschap", "Current version: ": "Huidige versie: ", "Download is disabled.": "Downloaden is uitgeschakeld." -} +} \ No newline at end of file diff --git a/locales/pl.json b/locales/pl.json index 1ba6f942..32ff0530 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -333,4 +333,4 @@ "Playlists": "Playlisty", "Community": "Społeczność", "Current version: ": "Aktualna wersja: " -} +} \ No newline at end of file diff --git a/locales/pt-BR.json b/locales/pt-BR.json index c3fa5a1e..9dd237c6 100644 --- a/locales/pt-BR.json +++ b/locales/pt-BR.json @@ -333,4 +333,4 @@ "Playlists": "Listas de reprodução", "Community": "Comunidade", "Current version: ": "Versão atual: " -} +} \ No newline at end of file diff --git a/locales/pt_PT.json b/locales/pt-PT.json similarity index 86% rename from locales/pt_PT.json rename to locales/pt-PT.json index 6e144551..ab7d3468 100644 --- a/locales/pt_PT.json +++ b/locales/pt-PT.json @@ -1,10 +1,16 @@ { - "`x` subscribers.([^.,0-9]|^)1([^.,0-9]|$)": "`x` subscritores.([^.,0-9]|^)1([^.,0-9]|$)", - "`x` subscribers.": "`x` subscritores.", - "`x` videos.([^.,0-9]|^)1([^.,0-9]|$)": "`x` vídeos.([^.,0-9]|^)1([^.,0-9]|$)", - "`x` videos.": "`x` vídeos.", - "`x` playlists.(\\D|^)1(\\D|$)": "`x` listas de reprodução.(\\D|^)1(\\D|$)", - "`x` playlists.": "`x` listas de reprodução.", + "`x` subscribers.": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` subscritores.", + "": "`x` subscritores." + }, + "`x` videos.": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` vídeos.", + "": "`x` vídeos." + }, + "`x` playlists.": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` listas de reprodução.", + "": "`x` listas de reprodução." + }, "LIVE": "Em direto", "Shared `x` ago": "Partilhado `x` atrás", "Unsubscribe": "Anular subscrição", @@ -116,16 +122,22 @@ "Subscription manager": "Gerir subscrições", "Token manager": "Gerir tokens", "Token": "Token", - "`x` subscriptions.([^.,0-9]|^)1([^.,0-9]|$)": "`x` subscrições.([^.,0-9]|^)1([^.,0-9]|$)", - "`x` subscriptions.": "`x` subscrições.", - "`x` tokens.([^.,0-9]|^)1([^.,0-9]|$)": "`x` tokens.([^.,0-9]|^)1([^.,0-9]|$)", - "`x` tokens.": "`x` tokens.", + "`x` subscriptions.": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` subscrições.", + "": "`x` subscrições." + }, + "`x` tokens.": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` tokens.", + "": "`x` tokens." + }, "Import/export": "Importar/Exportar", "unsubscribe": "Anular subscrição", "revoke": "revogar", "Subscriptions": "Subscrições", - "`x` unseen notifications.([^.,0-9]|^)1([^.,0-9]|$)": "`x` notificações não vistas.([^.,0-9]|^)1([^.,0-9]|$)", - "`x` unseen notifications.": "\"x\" notificações não vistas.", + "`x` unseen notifications.": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` notificações não vistas.", + "": "`x` notificações não vistas." + }, "search": "Pesquisar", "Log out": "Terminar sessão", "Released under the AGPLv3 by Omar Roth.": "Publicado sob a licença AGPLv3, por Omar Roth.", @@ -155,15 +167,19 @@ "Whitelisted regions: ": "Regiões permitidas: ", "Blacklisted regions: ": "Regiões bloqueadas: ", "Shared `x`": "Partilhado `x`", - "`x` views.([^.,0-9]|^)1([^.,0-9]|$)": "`x` visualizações.([^.,0-9]|^)1([^.,0-9]|$)", - "`x` views.": "`x` visualizações.", + "`x` views.": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` visualizações.", + "": "`x` visualizações." + }, "Premieres in `x`": "Estreias em 'x'", "Premieres `x`": "Estreias 'x'", "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Oi! Parece que JavaScript está desativado. Clique aqui para ver os comentários, entretanto eles podem levar mais tempo para carregar.", "View YouTube comments": "Ver comentários do YouTube", "View more comments on Reddit": "Ver mais comentários no Reddit", - "View `x` comments.(\\D|^)1(\\D|$)": "Ver `x` comentários.(\\D|^)1(\\D|$)", - "View `x` comments.": "Ver `x` comentários.", + "View `x` comments.": { + "([^.,0-9]|^)1([^.,0-9]|$)": "Ver `x` comentários.", + "": "Ver `x` comentários." + }, "View Reddit comments": "Ver comentários do Reddit", "Hide replies": "Ocultar respostas", "Show replies": "Mostrar respostas", @@ -188,12 +204,16 @@ "This channel does not exist.": "Este canal não existe.", "Could not get channel info.": "Não foi possível obter as informações do canal.", "Could not fetch comments": "Não foi possível obter os comentários", - "View `x` replies.([^.,0-9]|^)1([^.,0-9]|$)": "Ver `x` respostas.([^.,0-9]|^)1([^.,0-9]|$)", - "View `x` replies.": "Ver `x` respostas.", + "View `x` replies.": { + "([^.,0-9]|^)1([^.,0-9]|$)": "Ver `x` respostas.", + "": "Ver `x` respostas." + }, "`x` ago": "`x` atrás", "Load more": "Carregar mais", - "`x` points.([^.,0-9]|^)1([^.,0-9]|$)": "'x' pontos. ([^.,0-9]^)1(^.0-9][[$]", - "`x` points.": "'x' pontos.", + "`x` points.": { + "([^.,0-9]|^)1([^.,0-9]|$)": "'x' pontos.", + "": "'x' pontos." + }, "Could not create mix.": "Não foi possível criar mistura.", "Empty playlist": "Lista de reprodução vazia", "Not a playlist.": "Não é uma lista de reprodução.", @@ -311,20 +331,34 @@ "Yiddish": "Iídiche", "Yoruba": "Ioruba", "Zulu": "Zulu", - "`x` years.([^.,0-9]|^)1([^.,0-9]|$)": "`x` anos.([^.,0-9]|^)1([^.,0-9]|$)", - "`x` years.": "`x` anos.", - "`x` months.([^.,0-9]|^)1([^.,0-9]|$)": "`x` meses.([^.,0-9]|^)1([^.,0-9]|$)", - "`x` months.": "`x` meses.", - "`x` weeks.([^.,0-9]|^)1([^.,0-9]|$)": "`x` semanas.([^.,0-9]|^)1([^.,0-9]|$)", - "`x` weeks.": "`x` semanas.", - "`x` days.([^.,0-9]|^)1([^.,0-9]|$)": "`x` dias.([^.,0-9]|^)1([^.,0-9]|$)", - "`x` days.": "`x` dias.", - "`x` hours.([^.,0-9]|^)1([^.,0-9]|$)": "`x` horas.([^.,0-9]|^)1([^.,0-9]|$)", - "`x` hours.": "`x` horas.", - "`x` minutes.([^.,0-9]|^)1([^.,0-9]|$)": "`x` minutos.([^.,0-9]|^)1([^.,0-9]|$)", - "`x` minutes.": "`x` minutos.", - "`x` seconds.([^.,0-9]|^)1([^.,0-9]|$)": "`x` segundos.([^.,0-9]|^)1([^.,0-9]|$)", - "`x` seconds.": "`x` segundos.", + "`x` years.": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` anos.", + "": "`x` anos." + }, + "`x` months.": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` meses.", + "": "`x` meses." + }, + "`x` weeks.": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` semanas.", + "": "`x` semanas." + }, + "`x` days.": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` dias.", + "": "`x` dias." + }, + "`x` hours.": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` horas.", + "": "`x` horas." + }, + "`x` minutes.": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` minutos.", + "": "`x` minutos." + }, + "`x` seconds.": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` segundos.", + "": "`x` segundos." + }, "Fallback comments: ": "Comentários alternativos: ", "Popular": "Popular", "Top": "Top", @@ -350,4 +384,4 @@ "Playlists": "Listas de reprodução", "Community": "Comunidade", "Current version: ": "Versão atual: " -} +} \ No newline at end of file diff --git a/locales/ro.json b/locales/ro.json index 9539ff35..08d2c386 100644 --- a/locales/ro.json +++ b/locales/ro.json @@ -333,4 +333,4 @@ "Playlists": "Liste de redare", "Community": "Comunitate", "Current version: ": "Versiunea actuală: " -} +} \ No newline at end of file diff --git a/locales/ru.json b/locales/ru.json index 9cea15e0..e69b32e5 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -333,4 +333,4 @@ "Playlists": "Плейлисты", "Community": "Сообщество", "Current version: ": "Текущая версия: " -} +} \ No newline at end of file diff --git a/locales/sr_Cyrl.json b/locales/sr_Cyrl.json index 452ec266..786532df 100644 --- a/locales/sr_Cyrl.json +++ b/locales/sr_Cyrl.json @@ -1,9 +1,6 @@ { - "`x` subscribers.([^.,0-9]|^)1([^.,0-9]|$)": "", "`x` subscribers.": "", - "`x` videos.([^.,0-9]|^)1([^.,0-9]|$)": "", "`x` videos.": "", - "`x` playlists.(\\D|^)1(\\D|$)": "", "`x` playlists.": "", "LIVE": "", "Shared `x` ago": "", @@ -116,15 +113,12 @@ "Subscription manager": "", "Token manager": "", "Token": "", - "`x` subscriptions.([^.,0-9]|^)1([^.,0-9]|$)": "", "`x` subscriptions.": "", - "`x` tokens.([^.,0-9]|^)1([^.,0-9]|$)": "", "`x` tokens.": "", "Import/export": "", "unsubscribe": "", "revoke": "", "Subscriptions": "", - "`x` unseen notifications.([^.,0-9]|^)1([^.,0-9]|$)": "", "`x` unseen notifications.": "", "search": "", "Log out": "", @@ -155,14 +149,12 @@ "Whitelisted regions: ": "", "Blacklisted regions: ": "", "Shared `x`": "", - "`x` views.([^.,0-9]|^)1([^.,0-9]|$)": "", "`x` views.": "", "Premieres in `x`": "", "Premieres `x`": "", "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "", "View YouTube comments": "", "View more comments on Reddit": "", - "View `x` comments.(\\D|^)1(\\D|$)": "", "View `x` comments.": "", "View Reddit comments": "", "Hide replies": "", @@ -188,11 +180,9 @@ "This channel does not exist.": "", "Could not get channel info.": "", "Could not fetch comments": "", - "View `x` replies.([^.,0-9]|^)1([^.,0-9]|$)": "", "View `x` replies.": "", "`x` ago": "", "Load more": "", - "`x` points.([^.,0-9]|^)1([^.,0-9]|$)": "", "`x` points.": "", "Could not create mix.": "", "Empty playlist": "", @@ -311,19 +301,12 @@ "Yiddish": "", "Yoruba": "", "Zulu": "", - "`x` years.([^.,0-9]|^)1([^.,0-9]|$)": "", "`x` years.": "", - "`x` months.([^.,0-9]|^)1([^.,0-9]|$)": "", "`x` months.": "", - "`x` weeks.([^.,0-9]|^)1([^.,0-9]|$)": "", "`x` weeks.": "", - "`x` days.([^.,0-9]|^)1([^.,0-9]|$)": "", "`x` days.": "", - "`x` hours.([^.,0-9]|^)1([^.,0-9]|$)": "", "`x` hours.": "", - "`x` minutes.([^.,0-9]|^)1([^.,0-9]|$)": "", "`x` minutes.": "", - "`x` seconds.([^.,0-9]|^)1([^.,0-9]|$)": "", "`x` seconds.": "", "Fallback comments: ": "", "Popular": "", @@ -350,4 +333,4 @@ "Playlists": "", "Community": "", "Current version: ": "Тренутна верзија: " -} +} \ No newline at end of file diff --git a/locales/sv-SE.json b/locales/sv-SE.json index 55e87869..14e7d53e 100644 --- a/locales/sv-SE.json +++ b/locales/sv-SE.json @@ -333,4 +333,4 @@ "Playlists": "Spellistor", "Community": "Gemenskap", "Current version: ": "Nuvarande version: " -} +} \ No newline at end of file diff --git a/locales/tr.json b/locales/tr.json index b2794f65..652dff6d 100644 --- a/locales/tr.json +++ b/locales/tr.json @@ -341,4 +341,4 @@ "Playlists": "Oynatma listeleri", "Community": "Topluluk", "Current version: ": "Şu anki sürüm: " -} +} \ No newline at end of file diff --git a/locales/uk.json b/locales/uk.json index e2e4a63c..b04e0b2d 100644 --- a/locales/uk.json +++ b/locales/uk.json @@ -333,4 +333,4 @@ "Playlists": "Плейлисти", "Community": "Спільнота", "Current version: ": "Поточна версія: " -} +} \ No newline at end of file diff --git a/locales/zh-CN.json b/locales/zh-CN.json index fe12c65e..288f127d 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -333,4 +333,4 @@ "Playlists": "播放列表", "Community": "社区", "Current version: ": "当前版本:" -} +} \ No newline at end of file diff --git a/src/invidious.cr b/src/invidious.cr index 2d72f49f..2a493a46 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -83,6 +83,7 @@ LOCALES = { "es" => load_locale("es"), "eu" => load_locale("eu"), "fr" => load_locale("fr"), + "hu" => load_locale("hu-HU"), "is" => load_locale("is"), "it" => load_locale("it"), "ja" => load_locale("ja"), @@ -90,6 +91,7 @@ LOCALES = { "nl" => load_locale("nl"), "pl" => load_locale("pl"), "pt-BR" => load_locale("pt-BR"), + "pt-PT" => load_locale("pt-PT"), "ro" => load_locale("ro"), "ru" => load_locale("ru"), "sv" => load_locale("sv-SE"), From 9d23cf33fd6c062c055c0fd41141749eaa709a88 Mon Sep 17 00:00:00 2001 From: mendel5 <60322520+mendel5@users.noreply.github.com> Date: Thu, 30 Apr 2020 22:01:29 +0200 Subject: [PATCH 0108/2930] Consistent IDs for info section (#1133) --- src/invidious/views/watch.ecr | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 7743fd4a..7c9acb14 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -165,9 +165,9 @@ <% end %> -

<%= number_with_separator(video.views) %>

-

<%= number_with_separator(video.likes) %>

-

<%= number_with_separator(video.dislikes) %>

+

<%= number_with_separator(video.views) %>

+

<%= number_with_separator(video.likes) %>

+

<%= number_with_separator(video.dislikes) %>

<%= translate(locale, "Genre: ") %> <% if video.genre_url.empty? %> <%= video.genre %> From bd2c7e3bb900e6a9134c4fad08497b399195eb85 Mon Sep 17 00:00:00 2001 From: tleydxdy Date: Fri, 1 May 2020 09:35:34 +0800 Subject: [PATCH 0109/2930] Verify download, fix invidious file permission (#949) * Fix docker --- .travis.yml | 7 ++----- docker/Dockerfile | 29 ++++++++++++++--------------- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8b83db2a..403707c7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,8 +27,5 @@ jobs: install: - docker-compose build script: - - docker-compose up - - sleep 15 # Wait for cluster to become ready, TODO: do not sleep - - HEADERS="$(curl -I -s http://localhost:3000/)" - - STATUS="$(echo $HEADERS | head -n1)" - - if [[ "$STATUS" != *"200 OK"* ]]; then echo "$HEADERS"; exit 1; fi + - docker-compose up -d + - while curl -Isf http://localhost:3000; do sleep 1; done diff --git a/docker/Dockerfile b/docker/Dockerfile index 11ab6ed2..d0e4827a 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,27 +1,25 @@ FROM alpine:edge AS builder -RUN apk add --no-cache crystal shards libc-dev \ +RUN apk add --no-cache curl crystal shards libc-dev \ yaml-dev libxml2-dev sqlite-dev zlib-dev openssl-dev \ - sqlite-static zlib-static openssl-libs-static + yaml-static sqlite-static zlib-static openssl-libs-static WORKDIR /invidious -COPY ./shard.yml ./shard.yml -RUN shards update && shards install -RUN apk add --no-cache curl && \ - curl -Lo /etc/apk/keys/omarroth.rsa.pub https://github.com/omarroth/boringssl-alpine/releases/download/1.1.0-r0/omarroth.rsa.pub && \ +RUN curl -Lo /etc/apk/keys/omarroth.rsa.pub https://github.com/omarroth/boringssl-alpine/releases/download/1.1.0-r0/omarroth.rsa.pub && \ curl -Lo boringssl-dev.apk https://github.com/omarroth/boringssl-alpine/releases/download/1.1.0-r0/boringssl-dev-1.1.0-r0.apk && \ curl -Lo lsquic.apk https://github.com/omarroth/lsquic-alpine/releases/download/2.6.3-r0/lsquic-2.6.3-r0.apk && \ - tar -xf boringssl-dev.apk && \ - tar -xf lsquic.apk -RUN mv ./usr/lib/libcrypto.a ./lib/lsquic/src/lsquic/ext/libcrypto.a && \ - mv ./usr/lib/libssl.a ./lib/lsquic/src/lsquic/ext/libssl.a && \ - mv ./usr/lib/liblsquic.a ./lib/lsquic/src/lsquic/ext/liblsquic.a + apk verify --no-cache boringssl-dev.apk lsquic.apk && \ + tar -xf boringssl-dev.apk usr/lib/libcrypto.a usr/lib/libssl.a && \ + tar -xf lsquic.apk usr/lib/liblsquic.a && \ + rm /etc/apk/keys/omarroth.rsa.pub boringssl-dev.apk lsquic.apk +COPY ./shard.yml ./shard.yml +RUN shards update && shards install && \ + mv ./usr/lib/* ./lib/lsquic/src/lsquic/ext && \ + rm -r ./usr /root/.cache COPY ./src/ ./src/ # TODO: .git folder is required for building – this is destructive. # See definition of CURRENT_BRANCH, CURRENT_COMMIT and CURRENT_VERSION. COPY ./.git/ ./.git/ RUN crystal build ./src/invidious.cr \ --static --warnings all --error-on-warnings \ -# TODO: Remove next line, see https://github.com/crystal-lang/crystal/issues/7946 - -Dmusl \ --link-flags "-lxml2 -llzma" FROM alpine:latest @@ -30,10 +28,11 @@ WORKDIR /invidious RUN addgroup -g 1000 -S invidious && \ adduser -u 1000 -S invidious -G invidious COPY ./assets/ ./assets/ -COPY ./config/config.yml ./config/config.yml +COPY --chown=invidious ./config/config.yml ./config/config.yml +RUN sed -i 's/host: \(127.0.0.1\|localhost\)/host: postgres/' config/config.yml COPY ./config/sql/ ./config/sql/ COPY ./locales/ ./locales/ -RUN sed -i 's/host: \(127.0.0.1\|localhost\)/host: postgres/' config/config.yml COPY --from=builder /invidious/invidious . + USER invidious CMD [ "/invidious/invidious" ] From 75450dcdbcaf5f175147eb4f1e9db435804b593f Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Fri, 8 May 2020 08:59:09 -0500 Subject: [PATCH 0110/2930] Update signature param --- src/invidious/videos.cr | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index f9d3dc28..7e815ca1 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -562,8 +562,8 @@ struct Video if fmt_stream["url"]? fmt["url"] = fmt_stream["url"].as_s end - if fmt_stream["cipher"]? - HTTP::Params.parse(fmt_stream["cipher"].as_s).each do |key, value| + if cipher = fmt_stream["cipher"]? || fmt_stream["signatureCipher"]? + HTTP::Params.parse(cipher.as_s).each do |key, value| fmt[key] = value end end @@ -638,8 +638,8 @@ struct Video if adaptive_fmt["url"]? fmt["url"] = adaptive_fmt["url"].as_s end - if adaptive_fmt["cipher"]? - HTTP::Params.parse(adaptive_fmt["cipher"].as_s).each do |key, value| + if cipher = adaptive_fmt["cipher"]? || adaptive_fmt["signatureCipher"]? + HTTP::Params.parse(cipher.as_s).each do |key, value| fmt[key] = value end end From 454ae8656a61188082e54e75c637e99425e0e2ac Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Fri, 8 May 2020 09:00:53 -0500 Subject: [PATCH 0111/2930] Cleanup request headers --- src/invidious.cr | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index 2a493a46..6a197795 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -4538,10 +4538,8 @@ get "/api/v1/search/suggestions" do |env| query ||= "" begin - client = QUIC::Client.new("suggestqueries.google.com") - client.family = CONFIG.force_resolve || Socket::Family::INET - client.family = Socket::Family::INET if client.family == Socket::Family::UNSPEC - response = client.get("/complete/search?hl=en&gl=#{region}&client=youtube&ds=yt&q=#{URI.encode_www_form(query)}&callback=suggestCallback").body + headers = HTTP::Headers{":authority" => "suggestqueries.google.com"} + response = YT_POOL.client &.get("/complete/search?hl=en&gl=#{region}&client=youtube&ds=yt&q=#{URI.encode_www_form(query)}&callback=suggestCallback", headers).body body = response[35..-2] body = JSON.parse(body).as_a @@ -5671,8 +5669,7 @@ end get "/ggpht/*" do |env| url = env.request.path.lchop("/ggpht") - headers = HTTP::Headers.new - headers[":authority"] = "yt3.ggpht.com" + headers = HTTP::Headers{":authority" => "yt3.ggpht.com"} REQUEST_HEADERS_WHITELIST.each do |header| if env.request.headers[header]? headers[header] = env.request.headers[header] @@ -5758,8 +5755,7 @@ get "/s_p/:id/:name" do |env| url = env.request.resource - headers = HTTP::Headers.new - headers[":authority"] = "i9.ytimg.com" + headers = HTTP::Headers{":authority" => "i9.ytimg.com"} REQUEST_HEADERS_WHITELIST.each do |header| if env.request.headers[header]? headers[header] = env.request.headers[header] @@ -5822,8 +5818,7 @@ get "/vi/:id/:name" do |env| id = env.params.url["id"] name = env.params.url["name"] - headers = HTTP::Headers.new - headers[":authority"] = "i.ytimg.com" + headers = HTTP::Headers{":authority" => "i.ytimg.com"} if name == "maxres.jpg" build_thumbnails(id, config, Kemal.config).each do |thumb| @@ -5864,8 +5859,8 @@ get "/vi/:id/:name" do |env| end get "/Captcha" do |env| - client = make_client(LOGIN_URL) - response = client.get(env.request.resource) + headers = HTTP::Headers{":authority" => "accounts.google.com"} + response = YT_POOL.client &.get(env.request.resource, headers) env.response.headers["Content-Type"] = response.headers["Content-Type"] response.body end From 750ef296c63d20dfcd8ea311a44c5cfa28ca196e Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Wed, 13 May 2020 16:09:39 -0500 Subject: [PATCH 0112/2930] Update captcha handler --- src/invidious/helpers/jobs.cr | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/invidious/helpers/jobs.cr b/src/invidious/helpers/jobs.cr index 609e53c9..dc25d823 100644 --- a/src/invidious/helpers/jobs.cr +++ b/src/invidious/helpers/jobs.cr @@ -259,10 +259,7 @@ def bypass_captcha(captcha_key, logger) }, }.to_json).body) - if response["error"]? - raise response["error"].as_s - end - + raise response["error"].as_s if response["error"]? task_id = response["taskId"].as_i loop do @@ -286,8 +283,8 @@ def bypass_captcha(captcha_key, logger) yield response.cookies.select { |cookie| cookie.name != "PREF" } elsif response.headers["Location"]?.try &.includes?("/sorry/index") location = response.headers["Location"].try { |u| URI.parse(u) } - client = QUIC::Client.new(location.host.not_nil!) - response = client.get(location.full_path) + headers = HTTP::Headers{":authority" => location.host.not_nil!} + response = YT_POOL.client &.get(location.full_path, headers) html = XML.parse_html(response.body) form = html.xpath_node(%(//form[@action="index"])).not_nil! @@ -307,10 +304,7 @@ def bypass_captcha(captcha_key, logger) }, }.to_json).body) - if response["error"]? - raise response["error"].as_s - end - + raise response["error"].as_s if response["error"]? task_id = response["taskId"].as_i loop do @@ -329,8 +323,8 @@ def bypass_captcha(captcha_key, logger) end inputs["g-recaptcha-response"] = response["solution"]["gRecaptchaResponse"].as_s - client.close - client = QUIC::Client.new("www.google.com") + client = HTTPClient.new(location) + client.family = CONFIG.force_resolve || Socket::Family::INET response = client.post(location.full_path, form: inputs) headers = HTTP::Headers{ "Cookie" => URI.parse(response.headers["location"]).query_params["google_abuse"].split(";")[0], From 5d8de5fde2dee11ee8feb63f0bce74d373eec56f Mon Sep 17 00:00:00 2001 From: Dmitry Sandalov Date: Sun, 17 May 2020 14:28:00 +0300 Subject: [PATCH 0113/2930] Allow user to subscribe to playlist (#17) --- src/invidious.cr | 33 ++++++++++++++++++---- src/invidious/playlists.cr | 21 ++++++++++++++ src/invidious/views/playlist.ecr | 6 ++++ src/invidious/views/view_all_playlists.ecr | 18 ++++++++++-- src/invidious/views/watch.ecr | 2 +- 5 files changed, 71 insertions(+), 9 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index 6a197795..56722b7e 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -846,8 +846,14 @@ get "/view_all_playlists" do |env| user = user.as(User) - items = PG_DB.query_all("SELECT * FROM playlists WHERE author = $1 ORDER BY created", user.email, as: InvidiousPlaylist) - items.map! do |item| + items_created = PG_DB.query_all("SELECT * FROM playlists WHERE author = $1 AND id LIKE 'IV%' ORDER BY created", user.email, as: InvidiousPlaylist) + items_created.map! do |item| + item.author = "" + item + end + + items_saved = PG_DB.query_all("SELECT * FROM playlists WHERE author = $1 AND id NOT LIKE 'IV%' ORDER BY created", user.email, as: InvidiousPlaylist) + items_saved.map! do |item| item.author = "" item end @@ -918,6 +924,25 @@ post "/create_playlist" do |env| env.redirect "/playlist?list=#{playlist.id}" end +get "/subscribe_playlist" do |env| + locale = LOCALES[env.get("preferences").as(Preferences).locale]? + + user = env.get? "user" + referer = get_referer(env) + + if !user + next env.redirect "/" + end + + user = user.as(User) + + playlist_id = env.params.query["list"] + playlist = get_playlist(PG_DB, playlist_id, locale) + subscribe_playlist(PG_DB, user, playlist) + + env.redirect "/playlist?list=#{playlist.id}" +end + get "/delete_playlist" do |env| locale = LOCALES[env.get("preferences").as(Preferences).locale]? @@ -933,10 +958,6 @@ get "/delete_playlist" do |env| sid = sid.as(String) plid = env.params.query["list"]? - if !plid || !plid.starts_with?("IV") - next env.redirect referer - end - playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist) if !playlist || playlist.author != user.email next env.redirect referer diff --git a/src/invidious/playlists.cr b/src/invidious/playlists.cr index 9c8afd3c..184329dc 100644 --- a/src/invidious/playlists.cr +++ b/src/invidious/playlists.cr @@ -277,6 +277,27 @@ def create_playlist(db, title, privacy, user) return playlist end +def subscribe_playlist(db, user, playlist) + playlist = InvidiousPlaylist.new( + title: playlist.title.byte_slice(0, 150), + id: playlist.id, + author: user.email, + description: "", # Max 5000 characters + video_count: playlist.video_count, + created: Time.utc, + updated: playlist.updated, + privacy: PlaylistPrivacy::Private, + index: [] of Int64, + ) + + playlist_array = playlist.to_a + args = arg_array(playlist_array) + + db.exec("INSERT INTO playlists VALUES (#{args})", args: playlist_array) + + return playlist +end + def extract_playlist(plid, nodeset, index) videos = [] of PlaylistVideo diff --git a/src/invidious/views/playlist.ecr b/src/invidious/views/playlist.ecr index 7316af14..bb721c3a 100644 --- a/src/invidious/views/playlist.ecr +++ b/src/invidious/views/playlist.ecr @@ -45,6 +45,12 @@ <% if playlist.is_a?(InvidiousPlaylist) && playlist.author == user.try &.email %>

+ <% else %> + <% if PG_DB.query_one?("SELECT id FROM playlists WHERE id = $1", playlist.id, as: String).nil? %> +
+ <% else %> +
+ <% end %> <% end %>
diff --git a/src/invidious/views/view_all_playlists.ecr b/src/invidious/views/view_all_playlists.ecr index 0fa7a325..5ec6aa31 100644 --- a/src/invidious/views/view_all_playlists.ecr +++ b/src/invidious/views/view_all_playlists.ecr @@ -6,7 +6,7 @@
-

<%= translate(locale, "`x` playlists", %(#{items.size})) %>

+

<%= translate(locale, "`x` created playlists", %(#{items_created.size})) %>

@@ -16,7 +16,21 @@

- <% items.each_slice(4) do |slice| %> + <% items_created.each_slice(4) do |slice| %> + <% slice.each do |item| %> + <%= rendered "components/item" %> + <% end %> + <% end %> +
+ +
+
+

<%= translate(locale, "`x` saved playlists", %(#{items_saved.size})) %>

+
+
+ +
+ <% items_saved.each_slice(4) do |slice| %> <% slice.each do |item| %> <%= rendered "components/item" %> <% end %> diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 7c9acb14..61c3b7dc 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -102,7 +102,7 @@

<% if user %> - <% playlists = PG_DB.query_all("SELECT id,title FROM playlists WHERE author = $1", user.email, as: {String, String}) %> + <% playlists = PG_DB.query_all("SELECT id,title FROM playlists WHERE author = $1 AND id LIKE 'IV%'", user.email, as: {String, String}) %> <% if !playlists.empty? %>
From ceb252986e2f836f36d2f2d5a54c87ebec607120 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Mon, 25 May 2020 12:52:15 -0500 Subject: [PATCH 0114/2930] Update captcha job --- shard.yml | 2 +- src/invidious/helpers/jobs.cr | 14 +++++++++----- src/invidious/helpers/utils.cr | 9 ++++++--- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/shard.yml b/shard.yml index f3668ff4..59baf650 100644 --- a/shard.yml +++ b/shard.yml @@ -26,7 +26,7 @@ dependencies: version: ~> 0.1.2 lsquic: github: omarroth/lsquic.cr - version: ~> 0.1.9 + branch: dev crystal: 0.34.0 diff --git a/src/invidious/helpers/jobs.cr b/src/invidious/helpers/jobs.cr index dc25d823..c6e0ef42 100644 --- a/src/invidious/helpers/jobs.cr +++ b/src/invidious/helpers/jobs.cr @@ -295,7 +295,9 @@ def bypass_captcha(captcha_key, logger) inputs[node["name"]] = node["value"] end - response = JSON.parse(HTTP::Client.post("https://api.anti-captcha.com/createTask", body: { + captcha_client = HTTPClient.new(URI.parse("https://api.anti-captcha.com")) + captcha_client.family = CONFIG.force_resolve || Socket::Family::INET + response = JSON.parse(captcha_client.post("/createTask", body: { "clientKey" => CONFIG.captcha_key, "task" => { "type" => "NoCaptchaTaskProxyless", @@ -310,7 +312,7 @@ def bypass_captcha(captcha_key, logger) loop do sleep 10.seconds - response = JSON.parse(HTTP::Client.post("https://api.anti-captcha.com/getTaskResult", body: { + response = JSON.parse(captcha_client.post("/getTaskResult", body: { "clientKey" => CONFIG.captcha_key, "taskId" => task_id, }.to_json).body) @@ -323,9 +325,11 @@ def bypass_captcha(captcha_key, logger) end inputs["g-recaptcha-response"] = response["solution"]["gRecaptchaResponse"].as_s - client = HTTPClient.new(location) - client.family = CONFIG.force_resolve || Socket::Family::INET - response = client.post(location.full_path, form: inputs) + headers["content-type"] = "application/x-www-form-urlencoded" + headers["origin"] = "https://www.google.com" + headers["user-agent"] = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36" + + response = YT_POOL.client &.post("/sorry/index", headers: headers, form: inputs) headers = HTTP::Headers{ "Cookie" => URI.parse(response.headers["location"]).query_params["google_abuse"].split(";")[0], } diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index 79a69cf9..1fff206d 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -2,13 +2,16 @@ require "lsquic" require "pool/connection" def add_yt_headers(request) - request.headers["x-youtube-client-name"] ||= "1" - request.headers["x-youtube-client-version"] ||= "1.20180719" request.headers["user-agent"] ||= "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36" request.headers["accept-charset"] ||= "ISO-8859-1,utf-8;q=0.7,*;q=0.7" request.headers["accept"] ||= "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" request.headers["accept-language"] ||= "en-us,en;q=0.5" - request.headers["cookie"] = "#{(CONFIG.cookies.map { |c| "#{c.name}=#{c.value}" }).join("; ")}; #{request.headers["cookie"]?}" + return if request.resource.starts_with? "/sorry/index" + request.headers["x-youtube-client-name"] ||= "1" + request.headers["x-youtube-client-version"] ||= "1.20180719" + if !CONFIG.cookies.empty? + request.headers["cookie"] = "#{(CONFIG.cookies.map { |c| "#{c.name}=#{c.value}" }).join("; ")}; #{request.headers["cookie"]?}" + end end struct QUICPool From 80941eacbd72f328f7b1c77f4a936ebd9baa80fa Mon Sep 17 00:00:00 2001 From: mendel5 <60322520+mendel5@users.noreply.github.com> Date: Tue, 26 May 2020 09:57:10 +0200 Subject: [PATCH 0115/2930] More consistent HTML IDs for info section (#1156) * More consistent IDs for info section More consistent IDs for info section: watch-on-youtube, annotations and download * Consistent IDs: channel-name * Consistent IDs: published-date The term "published" can also be found in the answer for the following YouTube API request: https://developers.google.com/youtube/v3/docs/videos/list --- src/invidious/views/watch.ecr | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 7c9acb14..c46bf280 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -86,10 +86,10 @@
- + <%= translate(locale, "Watch on YouTube") %> -

+

<% if params.annotations %> <%= translate(locale, "Hide annotations") %> @@ -130,7 +130,7 @@ <% end %> <% if CONFIG.dmca_content.includes?(video.id) || CONFIG.disabled?("downloads") %> -

<%= translate(locale, "Download is disabled.") %>

+

<%= translate(locale, "Download is disabled.") %>

<% else %>
@@ -199,7 +199,7 @@
- <%= video.author %> + <%= video.author %>
@@ -208,7 +208,7 @@ <% sub_count_text = video.sub_count_text %> <%= rendered "components/subscribe_widget" %> -

+

<% if video.premiere_timestamp %> <%= translate(locale, "Premieres `x`", video.premiere_timestamp.not_nil!.to_s("%B %-d, %R UTC")) %> <% else %> From af7c57b082d3800343b313746e7a346c4d1051f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Allan=20Nordh=C3=B8y?= Date: Tue, 26 May 2020 18:02:21 +0000 Subject: [PATCH 0116/2930] TRANSLATION file for l10n --- TRANSLATION | 1 + 1 file changed, 1 insertion(+) create mode 100644 TRANSLATION diff --git a/TRANSLATION b/TRANSLATION new file mode 100644 index 00000000..fa340d71 --- /dev/null +++ b/TRANSLATION @@ -0,0 +1 @@ +https://hosted.weblate.org/projects/invidious/ From 6435c7b92161d573babd90cbe354b9671aeb5987 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Thu, 28 May 2020 11:47:48 -0500 Subject: [PATCH 0117/2930] Fix reCaptcha --- src/invidious/helpers/jobs.cr | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/invidious/helpers/jobs.cr b/src/invidious/helpers/jobs.cr index c6e0ef42..6479fa90 100644 --- a/src/invidious/helpers/jobs.cr +++ b/src/invidious/helpers/jobs.cr @@ -241,7 +241,8 @@ def bypass_captcha(captcha_key, logger) if response.body.includes?("To continue with your YouTube experience, please fill out the form below.") html = XML.parse_html(response.body) form = html.xpath_node(%(//form[@action="/das_captcha"])).not_nil! - site_key = form.xpath_node(%(.//div[@class="g-recaptcha"])).try &.["data-sitekey"] + site_key = form.xpath_node(%(.//div[@id="recaptcha"])).try &.["data-sitekey"] + s_value = form.xpath_node(%(.//div[@id="recaptcha"])).try &.["data-s"] inputs = {} of String => String form.xpath_nodes(%(.//input[@name])).map do |node| @@ -253,9 +254,10 @@ def bypass_captcha(captcha_key, logger) response = JSON.parse(HTTP::Client.post("https://api.anti-captcha.com/createTask", body: { "clientKey" => CONFIG.captcha_key, "task" => { - "type" => "NoCaptchaTaskProxyless", - "websiteURL" => "https://www.youtube.com/watch?v=CvFH_6DNRCY&gl=US&hl=en&disable_polymer=1&has_verified=1&bpctr=9999999999", - "websiteKey" => site_key, + "type" => "NoCaptchaTaskProxyless", + "websiteURL" => "https://www.youtube.com#{path}", + "websiteKey" => site_key, + "recaptchaDataSValue" => s_value, }, }.to_json).body) @@ -278,6 +280,7 @@ def bypass_captcha(captcha_key, logger) end inputs["g-recaptcha-response"] = response["solution"]["gRecaptchaResponse"].as_s + headers["Cookies"] = response["solution"]["cookies"].as_h.map { |k, v| "#{k}=#{v}" }.join("; ") response = YT_POOL.client &.post("/das_captcha", headers, form: inputs) yield response.cookies.select { |cookie| cookie.name != "PREF" } @@ -288,7 +291,8 @@ def bypass_captcha(captcha_key, logger) html = XML.parse_html(response.body) form = html.xpath_node(%(//form[@action="index"])).not_nil! - site_key = form.xpath_node(%(.//div[@class="g-recaptcha"])).try &.["data-sitekey"] + site_key = form.xpath_node(%(.//div[@id="recaptcha"])).try &.["data-sitekey"] + s_value = form.xpath_node(%(.//div[@id="recaptcha"])).try &.["data-s"] inputs = {} of String => String form.xpath_nodes(%(.//input[@name])).map do |node| @@ -300,9 +304,10 @@ def bypass_captcha(captcha_key, logger) response = JSON.parse(captcha_client.post("/createTask", body: { "clientKey" => CONFIG.captcha_key, "task" => { - "type" => "NoCaptchaTaskProxyless", - "websiteURL" => location.to_s, - "websiteKey" => site_key, + "type" => "NoCaptchaTaskProxyless", + "websiteURL" => location.to_s, + "websiteKey" => site_key, + "recaptchaDataSValue" => s_value, }, }.to_json).body) @@ -325,10 +330,7 @@ def bypass_captcha(captcha_key, logger) end inputs["g-recaptcha-response"] = response["solution"]["gRecaptchaResponse"].as_s - headers["content-type"] = "application/x-www-form-urlencoded" - headers["origin"] = "https://www.google.com" - headers["user-agent"] = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36" - + headers["Cookies"] = response["solution"]["cookies"].as_h.map { |k, v| "#{k}=#{v}" }.join("; ") response = YT_POOL.client &.post("/sorry/index", headers: headers, form: inputs) headers = HTTP::Headers{ "Cookie" => URI.parse(response.headers["location"]).query_params["google_abuse"].split(";")[0], From 8305af8f1036e9cb2ffc8aab5eb738d73c9f5654 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Fri, 29 May 2020 20:06:43 -0500 Subject: [PATCH 0118/2930] Update docker build --- docker/Dockerfile | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index d0e4827a..505b8d88 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -3,17 +3,14 @@ RUN apk add --no-cache curl crystal shards libc-dev \ yaml-dev libxml2-dev sqlite-dev zlib-dev openssl-dev \ yaml-static sqlite-static zlib-static openssl-libs-static WORKDIR /invidious -RUN curl -Lo /etc/apk/keys/omarroth.rsa.pub https://github.com/omarroth/boringssl-alpine/releases/download/1.1.0-r0/omarroth.rsa.pub && \ - curl -Lo boringssl-dev.apk https://github.com/omarroth/boringssl-alpine/releases/download/1.1.0-r0/boringssl-dev-1.1.0-r0.apk && \ - curl -Lo lsquic.apk https://github.com/omarroth/lsquic-alpine/releases/download/2.6.3-r0/lsquic-2.6.3-r0.apk && \ - apk verify --no-cache boringssl-dev.apk lsquic.apk && \ - tar -xf boringssl-dev.apk usr/lib/libcrypto.a usr/lib/libssl.a && \ - tar -xf lsquic.apk usr/lib/liblsquic.a && \ - rm /etc/apk/keys/omarroth.rsa.pub boringssl-dev.apk lsquic.apk COPY ./shard.yml ./shard.yml RUN shards update && shards install && \ - mv ./usr/lib/* ./lib/lsquic/src/lsquic/ext && \ - rm -r ./usr /root/.cache + # TODO: Document build instructions + # See https://github.com/omarroth/boringssl-alpine/blob/master/APKBUILD, + # https://github.com/omarroth/lsquic-alpine/blob/master/APKBUILD, + # https://github.com/omarroth/lsquic.cr/issues/1#issuecomment-631610081 + # for details building static lib + curl -Lo ./lib/lsquic/src/lsquic/ext/liblsquic.a https://omar.yt/lsquic/liblsquic.a COPY ./src/ ./src/ # TODO: .git folder is required for building – this is destructive. # See definition of CURRENT_BRANCH, CURRENT_COMMIT and CURRENT_VERSION. From c422a6dd4ff47779425793fd48a7ee03b52260fd Mon Sep 17 00:00:00 2001 From: Sandro Date: Sat, 6 Jun 2020 04:12:43 +0200 Subject: [PATCH 0119/2930] Add RAM requirement Closes #1152 --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d8cc1c6e..3526e581 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,9 @@ $ docker volume rm invidious_postgresdata $ docker-compose build ``` -### Linux: +### Linux + +To manually compile invidious you need at least 2GB of RAM. If you have less please fall back to Docker. #### Install dependencies From 24013af3bb84368dda0e65c5e2fb8df0cb5348c6 Mon Sep 17 00:00:00 2001 From: Sandro Date: Mon, 15 Jun 2020 19:24:35 +0200 Subject: [PATCH 0120/2930] Mention SWAP --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3526e581..afe59c51 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ $ docker-compose build ### Linux -To manually compile invidious you need at least 2GB of RAM. If you have less please fall back to Docker. +To manually compile invidious you need at least 2GB of RAM. If you have less you can setup SWAP to have a combined amount of 2 GB or fall back to Docker. #### Install dependencies From d30a972a909e66d963ee953349fe045a1d9a41ee Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Mon, 15 Jun 2020 17:57:20 -0500 Subject: [PATCH 0121/2930] Support Crystal 0.35.0 --- shard.yml | 6 +-- src/invidious.cr | 4 +- src/invidious/helpers/handlers.cr | 4 +- src/invidious/helpers/helpers.cr | 50 +++++++++++++++----- src/invidious/helpers/static_file_handler.cr | 4 +- src/invidious/helpers/utils.cr | 2 +- 6 files changed, 48 insertions(+), 22 deletions(-) diff --git a/shard.yml b/shard.yml index 59baf650..2c1e54aa 100644 --- a/shard.yml +++ b/shard.yml @@ -11,13 +11,13 @@ targets: dependencies: pg: github: will/crystal-pg - version: ~> 0.21.0 + version: ~> 0.21.1 sqlite3: github: crystal-lang/crystal-sqlite3 version: ~> 0.16.0 kemal: github: kemalcr/kemal - version: ~> 0.26.1 + branch: master pool: github: ysbaddaden/pool version: ~> 0.2.3 @@ -28,6 +28,6 @@ dependencies: github: omarroth/lsquic.cr branch: dev -crystal: 0.34.0 +crystal: 0.35.0 license: AGPLv3 diff --git a/src/invidious.cr b/src/invidious.cr index 56722b7e..75d1e0d1 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -23,7 +23,7 @@ require "pg" require "sqlite3" require "xml" require "yaml" -require "zip" +require "compress/zip" require "protodec/utils" require "./invidious/helpers/*" require "./invidious/*" @@ -2660,7 +2660,7 @@ post "/data_control" do |env| PG_DB.exec("UPDATE users SET feed_needs_update = true, subscriptions = $1 WHERE email = $2", user.subscriptions, user.email) when "import_newpipe" - Zip::Reader.open(IO::Memory.new(body)) do |file| + Compress::Zip::Reader.open(IO::Memory.new(body)) do |file| file.each_entry do |entry| if entry.filename == "newpipe.db" tempfile = File.tempfile(".db") diff --git a/src/invidious/helpers/handlers.cr b/src/invidious/helpers/handlers.cr index 87b10bc9..d0b6c5a3 100644 --- a/src/invidious/helpers/handlers.cr +++ b/src/invidious/helpers/handlers.cr @@ -74,10 +74,10 @@ class FilteredCompressHandler < Kemal::Handler if request_headers.includes_word?("Accept-Encoding", "gzip") env.response.headers["Content-Encoding"] = "gzip" - env.response.output = Gzip::Writer.new(env.response.output, sync_close: true) + env.response.output = Compress::Gzip::Writer.new(env.response.output, sync_close: true) elsif request_headers.includes_word?("Accept-Encoding", "deflate") env.response.headers["Content-Encoding"] = "deflate" - env.response.output = Flate::Writer.new(env.response.output, sync_close: true) + env.response.output = Compress::Deflate::Writer.new(env.response.output, sync_close: true) end call_next env diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr index 96d14737..f16b5c6e 100644 --- a/src/invidious/helpers/helpers.cr +++ b/src/invidious/helpers/helpers.cr @@ -625,6 +625,7 @@ def extract_shelf_items(nodeset, ucid = nil, author_name = nil) end def check_enum(db, logger, enum_name, struct_type = nil) + return # TODO if !db.query_one?("SELECT true FROM pg_type WHERE typname = $1", enum_name, as: Bool) logger.puts("CREATE TYPE #{enum_name}") @@ -646,18 +647,14 @@ def check_table(db, logger, table_name, struct_type = nil) end end - if !struct_type - return - end + return if !struct_type struct_array = struct_type.to_type_tuple column_array = get_column_array(db, table_name) column_types = File.read("config/sql/#{table_name}.sql").match(/CREATE TABLE public\.#{table_name}\n\((?[\d\D]*?)\);/) - .try &.["types"].split(",").map { |line| line.strip } + .try &.["types"].split(",").map { |line| line.strip }.reject &.starts_with?("CONSTRAINT") - if !column_types - return - end + return if !column_types struct_array.each_with_index do |name, i| if name != column_array[i]? @@ -708,6 +705,15 @@ def check_table(db, logger, table_name, struct_type = nil) end end end + + return if column_array.size <= struct_array.size + + # column_array.each do |column| + # if !struct_array.includes? column + # logger.puts("ALTER TABLE #{table_name} DROP COLUMN #{column} CASCADE") + # db.exec("ALTER TABLE #{table_name} DROP COLUMN #{column} CASCADE") + # end + # end end class PG::ResultSet @@ -897,15 +903,35 @@ end def proxy_file(response, env) if response.headers.includes_word?("Content-Encoding", "gzip") - Gzip::Writer.open(env.response) do |deflate| - response.pipe(deflate) + Compress::Gzip::Writer.open(env.response) do |deflate| + IO.copy response.body_io, deflate end elsif response.headers.includes_word?("Content-Encoding", "deflate") - Flate::Writer.open(env.response) do |deflate| - response.pipe(deflate) + Compress::Deflate::Writer.open(env.response) do |deflate| + IO.copy response.body_io, deflate end else - response.pipe(env.response) + IO.copy response.body_io, env.response + end +end + +# See https://github.com/kemalcr/kemal/pull/576 +class HTTP::Server::Response::Output + def close + return if closed? + + unless response.wrote_headers? + response.content_length = @out_count + end + + ensure_headers_written + + super + + if @chunked + @io << "0\r\n\r\n" + @io.flush + end end end diff --git a/src/invidious/helpers/static_file_handler.cr b/src/invidious/helpers/static_file_handler.cr index 20d92b9c..be9d36ab 100644 --- a/src/invidious/helpers/static_file_handler.cr +++ b/src/invidious/helpers/static_file_handler.cr @@ -81,12 +81,12 @@ def send_file(env : HTTP::Server::Context, file_path : String, data : Slice(UInt condition = config.is_a?(Hash) && config["gzip"]? == true && filesize > minsize && Kemal::Utils.zip_types(file_path) if condition && request_headers.includes_word?("Accept-Encoding", "gzip") env.response.headers["Content-Encoding"] = "gzip" - Gzip::Writer.open(env.response) do |deflate| + Compress::Gzip::Writer.open(env.response) do |deflate| IO.copy(file, deflate) end elsif condition && request_headers.includes_word?("Accept-Encoding", "deflate") env.response.headers["Content-Encoding"] = "deflate" - Flate::Writer.open(env.response) do |deflate| + Compress::Deflate::Writer.open(env.response) do |deflate| IO.copy(file, deflate) end else diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index 1fff206d..a4bd1d54 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -332,7 +332,7 @@ end def sha256(text) digest = OpenSSL::Digest.new("SHA256") digest << text - return digest.hexdigest + return digest.final.hexstring end def subscribe_pubsub(topic, key, config) From 4d4b6a2fa03b9de428a931729600c252ed801c36 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Mon, 15 Jun 2020 17:00:34 -0500 Subject: [PATCH 0122/2930] Remove top page --- src/invidious.cr | 62 +---------------------------- src/invidious/helpers/helpers.cr | 25 ------------ src/invidious/helpers/jobs.cr | 35 ---------------- src/invidious/views/preferences.ecr | 5 --- src/invidious/views/top.ecr | 20 ---------- 5 files changed, 2 insertions(+), 145 deletions(-) delete mode 100644 src/invidious/views/top.ecr diff --git a/src/invidious.cr b/src/invidious.cr index 75d1e0d1..6b408dc6 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -195,15 +195,6 @@ if config.statistics_enabled end end -top_videos = [] of Video -if config.top_enabled - spawn do - pull_top_videos(config, PG_DB) do |videos| - top_videos = videos - end - end -end - popular_videos = [] of ChannelVideo spawn do pull_popular_videos(PG_DB) do |videos| @@ -367,12 +358,6 @@ get "/" do |env| templated "empty" when "Popular" templated "popular" - when "Top" - if config.top_enabled - templated "top" - else - templated "empty" - end when "Trending" env.redirect "/feed/trending" when "Subscriptions" @@ -2123,10 +2108,6 @@ post "/preferences" do |env| end config.default_user_preferences.feed_menu = admin_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" @@ -3044,12 +3025,7 @@ end get "/feed/top" do |env| locale = LOCALES[env.get("preferences").as(Preferences).locale]? - - if config.top_enabled - templated "top" - else - env.redirect "/" - end + env.redirect "/" end get "/feed/popular" do |env| @@ -4171,41 +4147,7 @@ get "/api/v1/top" do |env| locale = LOCALES[env.get("preferences").as(Preferences).locale]? env.response.content_type = "application/json" - - if !config.top_enabled - error_message = {"error" => "Administrator has disabled this endpoint."}.to_json - env.response.status_code = 400 - next error_message - end - - JSON.build do |json| - json.array do - top_videos.each do |video| - # Top videos have much more information than provided below (adaptiveFormats, etc) - # but can be very out of date, so we only provide a subset here - - json.object do - json.field "title", video.title - json.field "videoId", video.id - json.field "videoThumbnails" do - generate_thumbnails(json, video.id, config, Kemal.config) - end - - json.field "lengthSeconds", video.length_seconds - json.field "viewCount", video.views - - json.field "author", video.author - json.field "authorId", video.ucid - json.field "authorUrl", "/channel/#{video.ucid}" - json.field "published", video.published.to_unix - json.field "publishedText", translate(locale, "`x` ago", recode_date(video.published, locale)) - - json.field "description", html_to_content(video.description_html) - json.field "descriptionHtml", video.description_html - end - end - end - end + "[]" end get "/api/v1/channels/:ucid" do |env| diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr index f16b5c6e..f6ba33cd 100644 --- a/src/invidious/helpers/helpers.cr +++ b/src/invidious/helpers/helpers.cr @@ -239,7 +239,6 @@ struct Config hmac_key: String?, # HMAC signing key for CSRF tokens and verifying pubsub subscriptions domain: String?, # Domain to be used for links to resources on the site where an absolute URL is required use_pubsub_feeds: {type: Bool | Int32, default: false}, # Subscribe to channels using PubSubHubbub (requires domain, hmac_key) - top_enabled: {type: Bool, default: true}, captcha_enabled: {type: Bool, default: true}, login_enabled: {type: Bool, default: true}, registration_enabled: {type: Bool, default: true}, @@ -276,30 +275,6 @@ struct DBConfig }) end -def rank_videos(db, n) - top = [] of {Float64, String} - - db.query("SELECT id, wilson_score, published FROM videos WHERE views > 5000 ORDER BY published DESC LIMIT 1000") do |rs| - rs.each do - id = rs.read(String) - wilson_score = rs.read(Float64) - published = rs.read(Time) - - # Exponential decay, older videos tend to rank lower - temperature = wilson_score * Math.exp(-0.000005*((Time.utc - published).total_minutes)) - top << {temperature, id} - end - end - - top.sort! - - # Make hottest come first - top.reverse! - top = top.map { |a, b| b } - - return top[0..n - 1] -end - def login_req(f_req) data = { # Unfortunately there's not much information available on `bgRequest`; part of Google's BotGuard diff --git a/src/invidious/helpers/jobs.cr b/src/invidious/helpers/jobs.cr index 6479fa90..a9aee064 100644 --- a/src/invidious/helpers/jobs.cr +++ b/src/invidious/helpers/jobs.cr @@ -170,41 +170,6 @@ def subscribe_to_feeds(db, logger, key, config) end end -def pull_top_videos(config, db) - loop do - begin - top = rank_videos(db, 40) - rescue ex - sleep 1.minute - Fiber.yield - - next - end - - if top.size == 0 - sleep 1.minute - Fiber.yield - - next - end - - videos = [] of Video - - top.each do |id| - begin - videos << get_video(id, db) - rescue ex - next - end - end - - yield videos - - sleep 1.minute - Fiber.yield - end -end - def pull_popular_videos(db) loop do videos = db.query_all("SELECT DISTINCT ON (ucid) * FROM channel_videos WHERE ucid IN \ diff --git a/src/invidious/views/preferences.ecr b/src/invidious/views/preferences.ecr index 7e899133..fb5bd44b 100644 --- a/src/invidious/views/preferences.ecr +++ b/src/invidious/views/preferences.ecr @@ -227,11 +227,6 @@ <% end %>

-
- - checked<% end %>> -
-
checked<% end %>> diff --git a/src/invidious/views/top.ecr b/src/invidious/views/top.ecr deleted file mode 100644 index f5db3aaa..00000000 --- a/src/invidious/views/top.ecr +++ /dev/null @@ -1,20 +0,0 @@ -<% content_for "header" do %> -"> - - <% if env.get("preferences").as(Preferences).default_home != "Top" %> - <%= translate(locale, "Top") %> - Invidious - <% else %> - Invidious - <% end %> - -<% end %> - -<%= rendered "components/feed_menu" %> - -
- <% top_videos.each_slice(4) do |slice| %> - <% slice.each do |item| %> - <%= rendered "components/item" %> - <% end %> - <% end %> -
From c1cbdae5ee52669f7c52c3b477a1649d014d4684 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Mon, 15 Jun 2020 17:10:30 -0500 Subject: [PATCH 0123/2930] Make HOST_URL constant --- src/invidious.cr | 123 ++++++++++++---------------- src/invidious/channels.cr | 58 +++++-------- src/invidious/helpers/helpers.cr | 13 ++- src/invidious/helpers/signatures.cr | 8 +- src/invidious/helpers/utils.cr | 4 +- src/invidious/playlists.cr | 50 +++++------ src/invidious/search.cr | 53 ++++++------ src/invidious/videos.cr | 37 ++++----- src/invidious/views/watch.ecr | 16 ++-- 9 files changed, 163 insertions(+), 199 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index 6b408dc6..958f95f7 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -50,6 +50,7 @@ PUBSUB_URL = URI.parse("https://pubsubhubbub.appspot.com") REDDIT_URL = URI.parse("https://www.reddit.com") TEXTCAPTCHA_URL = URI.parse("https://textcaptcha.com") YT_URL = URI.parse("https://www.youtube.com") +HOST_URL = make_host_url(CONFIG, Kemal.config) CHARS_SAFE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" TEST_IDS = {"AgbeGFYluEA", "BaW_jenozKc", "a9LDPn-MO4I", "ddFvjfvPnqk", "iqKdEhx-dD4"} @@ -202,10 +203,11 @@ spawn do end end -decrypt_function = [] of {SigProc, Int32} +DECRYPT_FUNCTION = [] of {SigProc, Int32} spawn do update_decrypt_function do |function| - decrypt_function = function + DECRYPT_FUNCTION.clear + function.each { |i| DECRYPT_FUNCTION << i } end end @@ -1351,16 +1353,14 @@ get "/opensearch.xml" do |env| locale = LOCALES[env.get("preferences").as(Preferences).locale]? env.response.content_type = "application/opensearchdescription+xml" - host = make_host_url(config, Kemal.config) - XML.build(indent: " ", encoding: "UTF-8") do |xml| xml.element("OpenSearchDescription", xmlns: "http://a9.com/-/spec/opensearch/1.1/") do xml.element("ShortName") { xml.text "Invidious" } xml.element("LongName") { xml.text "Invidious Search" } xml.element("Description") { xml.text "Search for videos, channels, and playlists on Invidious" } xml.element("InputEncoding") { xml.text "UTF-8" } - xml.element("Image", width: 48, height: 48, type: "image/x-icon") { xml.text "#{host}/favicon.ico" } - xml.element("Url", type: "text/html", method: "get", template: "#{host}/search?q={searchTerms}") + xml.element("Image", width: 48, height: 48, type: "image/x-icon") { xml.text "#{HOST_URL}/favicon.ico" } + xml.element("Url", type: "text/html", method: "get", template: "#{HOST_URL}/search?q={searchTerms}") end end end @@ -2473,8 +2473,6 @@ get "/subscription_manager" do |env| subscriptions.sort_by! { |channel| channel.author.downcase } if action_takeout - host_url = make_host_url(config, Kemal.config) - if format == "json" env.response.content_type = "application/json" env.response.headers["content-disposition"] = "attachment" @@ -2500,7 +2498,7 @@ get "/subscription_manager" do |env| if format == "newpipe" xmlUrl = "https://www.youtube.com/feeds/videos.xml?channel_id=#{channel.id}" else - xmlUrl = "#{host_url}/feed/channel/#{channel.id}" + xmlUrl = "#{HOST_URL}/feed/channel/#{channel.id}" end xml.element("outline", text: channel.author, title: channel.author, @@ -3179,25 +3177,23 @@ get "/feed/channel/:ucid" do |env| ) end - host_url = make_host_url(config, Kemal.config) - XML.build(indent: " ", encoding: "UTF-8") do |xml| xml.element("feed", "xmlns:yt": "http://www.youtube.com/xml/schemas/2015", "xmlns:media": "http://search.yahoo.com/mrss/", xmlns: "http://www.w3.org/2005/Atom", "xml:lang": "en-US") do - xml.element("link", rel: "self", href: "#{host_url}#{env.request.resource}") + xml.element("link", rel: "self", href: "#{HOST_URL}#{env.request.resource}") xml.element("id") { xml.text "yt:channel:#{channel.ucid}" } xml.element("yt:channelId") { xml.text channel.ucid } xml.element("title") { xml.text channel.author } - xml.element("link", rel: "alternate", href: "#{host_url}/channel/#{channel.ucid}") + xml.element("link", rel: "alternate", href: "#{HOST_URL}/channel/#{channel.ucid}") xml.element("author") do xml.element("name") { xml.text channel.author } - xml.element("uri") { xml.text "#{host_url}/channel/#{channel.ucid}" } + xml.element("uri") { xml.text "#{HOST_URL}/channel/#{channel.ucid}" } end videos.each do |video| - video.to_xml(host_url, channel.auto_generated, params, xml) + video.to_xml(channel.auto_generated, params, xml) end end end @@ -3231,19 +3227,18 @@ get "/feed/private" do |env| params = HTTP::Params.parse(env.params.query["params"]? || "") videos, notifications = get_subscription_feed(PG_DB, user, max_results, page) - host_url = make_host_url(config, Kemal.config) XML.build(indent: " ", encoding: "UTF-8") do |xml| xml.element("feed", "xmlns:yt": "http://www.youtube.com/xml/schemas/2015", "xmlns:media": "http://search.yahoo.com/mrss/", xmlns: "http://www.w3.org/2005/Atom", "xml:lang": "en-US") do - xml.element("link", "type": "text/html", rel: "alternate", href: "#{host_url}/feed/subscriptions") + xml.element("link", "type": "text/html", rel: "alternate", href: "#{HOST_URL}/feed/subscriptions") xml.element("link", "type": "application/atom+xml", rel: "self", - href: "#{host_url}#{env.request.resource}") + href: "#{HOST_URL}#{env.request.resource}") xml.element("title") { xml.text translate(locale, "Invidious Private Feed for `x`", user.email) } (notifications + videos).each do |video| - video.to_xml(locale, host_url, params, xml) + video.to_xml(locale, params, xml) end end end @@ -3257,8 +3252,6 @@ get "/feed/playlist/:plid" do |env| plid = env.params.url["plid"] params = HTTP::Params.parse(env.params.query["params"]? || "") - - host_url = make_host_url(config, Kemal.config) path = env.request.path if plid.starts_with? "IV" @@ -3269,18 +3262,18 @@ get "/feed/playlist/:plid" do |env| xml.element("feed", "xmlns:yt": "http://www.youtube.com/xml/schemas/2015", "xmlns:media": "http://search.yahoo.com/mrss/", xmlns: "http://www.w3.org/2005/Atom", "xml:lang": "en-US") do - xml.element("link", rel: "self", href: "#{host_url}#{env.request.resource}") + xml.element("link", rel: "self", href: "#{HOST_URL}#{env.request.resource}") xml.element("id") { xml.text "iv:playlist:#{plid}" } xml.element("iv:playlistId") { xml.text plid } xml.element("title") { xml.text playlist.title } - xml.element("link", rel: "alternate", href: "#{host_url}/playlist?list=#{plid}") + xml.element("link", rel: "alternate", href: "#{HOST_URL}/playlist?list=#{plid}") xml.element("author") do xml.element("name") { xml.text playlist.author } end videos.each do |video| - video.to_xml(host_url, false, xml) + video.to_xml(false, xml) end end end @@ -3299,7 +3292,7 @@ get "/feed/playlist/:plid" do |env| when "url", "href" full_path = URI.parse(node[attribute.name]).full_path query_string_opt = full_path.starts_with?("/watch?v=") ? "&#{params}" : "" - node[attribute.name] = "#{host_url}#{full_path}#{query_string_opt}" + node[attribute.name] = "#{HOST_URL}#{full_path}#{query_string_opt}" else nil # Skip end end @@ -3308,7 +3301,7 @@ get "/feed/playlist/:plid" do |env| document = document.to_xml(options: XML::SaveOptions::NO_DECL) document.scan(/(?[^<]+)<\/uri>/).each do |match| - content = "#{host_url}#{URI.parse(match["url"]).full_path}" + content = "#{HOST_URL}#{URI.parse(match["url"]).full_path}" document = document.gsub(match[0], "#{content}") end @@ -3684,7 +3677,7 @@ get "/channel/:ucid/community" do |env| end begin - items = JSON.parse(fetch_channel_community(ucid, continuation, locale, config, Kemal.config, "json", thin_mode)) + items = JSON.parse(fetch_channel_community(ucid, continuation, locale, "json", thin_mode)) rescue ex env.response.status_code = 500 error_message = ex.message @@ -3737,7 +3730,6 @@ get "/api/v1/storyboards/:id" do |env| end storyboards = video.storyboards - width = env.params.query["width"]? height = env.params.query["height"]? @@ -3745,7 +3737,7 @@ get "/api/v1/storyboards/:id" do |env| response = JSON.build do |json| json.object do json.field "storyboards" do - generate_storyboards(json, id, storyboards, config, Kemal.config) + generate_storyboards(json, id, storyboards) end end end @@ -3775,8 +3767,7 @@ get "/api/v1/storyboards/:id" do |env| end_time = storyboard[:interval].milliseconds storyboard[:storyboard_count].times do |i| - host_url = make_host_url(config, Kemal.config) - url = storyboard[:url].gsub("$M", i).gsub("https://i9.ytimg.com", host_url) + url = storyboard[:url].gsub("$M", i).gsub("https://i9.ytimg.com", HOST_URL) storyboard[:storyboard_height].times do |j| storyboard[:storyboard_width].times do |k| @@ -4099,7 +4090,7 @@ get "/api/v1/videos/:id" do |env| next error_message end - video.to_json(locale, config, Kemal.config, decrypt_function) + video.to_json(locale) end get "/api/v1/trending" do |env| @@ -4121,7 +4112,7 @@ get "/api/v1/trending" do |env| videos = JSON.build do |json| json.array do trending.each do |video| - video.to_json(locale, config, Kemal.config, json) + video.to_json(locale, json) end end end @@ -4137,7 +4128,7 @@ get "/api/v1/popular" do |env| JSON.build do |json| json.array do popular_videos.each do |video| - video.to_json(locale, config, Kemal.config, json) + video.to_json(locale, json) end end end @@ -4178,7 +4169,7 @@ get "/api/v1/channels/:ucid" do |env| count = 0 else begin - videos, count = get_60_videos(channel.ucid, channel.author, page, channel.auto_generated, sort_by) + count, videos = get_60_videos(channel.ucid, channel.author, page, channel.auto_generated, sort_by) rescue ex error_message = {"error" => ex.message}.to_json env.response.status_code = 500 @@ -4247,7 +4238,7 @@ get "/api/v1/channels/:ucid" do |env| json.field "latestVideos" do json.array do videos.each do |video| - video.to_json(locale, config, Kemal.config, json) + video.to_json(locale, json) end end end @@ -4308,7 +4299,7 @@ end end begin - videos, count = get_60_videos(channel.ucid, channel.author, page, channel.auto_generated, sort_by) + count, videos = get_60_videos(channel.ucid, channel.author, page, channel.auto_generated, sort_by) rescue ex error_message = {"error" => ex.message}.to_json env.response.status_code = 500 @@ -4318,7 +4309,7 @@ end JSON.build do |json| json.array do videos.each do |video| - video.to_json(locale, config, Kemal.config, json) + video.to_json(locale, json) end end end @@ -4344,7 +4335,7 @@ end JSON.build do |json| json.array do videos.each do |video| - video.to_json(locale, config, Kemal.config, json) + video.to_json(locale, json) end end end @@ -4359,9 +4350,9 @@ end ucid = env.params.url["ucid"] continuation = env.params.query["continuation"]? - sort_by = env.params.query["sort"]?.try &.downcase - sort_by ||= env.params.query["sort_by"]?.try &.downcase - sort_by ||= "last" + sort_by = env.params.query["sort"]?.try &.downcase || + env.params.query["sort_by"]?.try &.downcase || + "last" begin channel = get_about_info(ucid, locale) @@ -4383,9 +4374,7 @@ end json.field "playlists" do json.array do items.each do |item| - if item.is_a?(SearchPlaylist) - item.to_json(locale, config, Kemal.config, json) - end + item.to_json(locale, json) if item.is_a?(SearchPlaylist) end end end @@ -4414,7 +4403,7 @@ end # sort_by = env.params.query["sort_by"]?.try &.downcase begin - fetch_channel_community(ucid, continuation, locale, config, Kemal.config, format, thin_mode) + fetch_channel_community(ucid, continuation, locale, format, thin_mode) rescue ex env.response.status_code = 400 error_message = {"error" => ex.message}.to_json @@ -4440,7 +4429,7 @@ get "/api/v1/channels/search/:ucid" do |env| JSON.build do |json| json.array do search_results.each do |item| - item.to_json(locale, config, Kemal.config, json) + item.to_json(locale, json) end end end @@ -4485,7 +4474,7 @@ get "/api/v1/search" do |env| JSON.build do |json| json.array do search_results.each do |item| - item.to_json(locale, config, Kemal.config, json) + item.to_json(locale, json) end end end @@ -4562,7 +4551,7 @@ end next error_message end - response = playlist.to_json(offset, locale, config, Kemal.config, continuation: continuation) + response = playlist.to_json(offset, locale, continuation: continuation) if format == "html" response = JSON.parse(response) @@ -4626,7 +4615,7 @@ get "/api/v1/mixes/:rdid" do |env| json.field "videoThumbnails" do json.array do - generate_thumbnails(json, video.id, config, Kemal.config) + generate_thumbnails(json, video.id) end end @@ -4661,7 +4650,7 @@ get "/api/v1/auth/notifications" do |env| topics = env.params.query["topics"]?.try &.split(",").uniq.first(1000) topics ||= [] of String - create_notification_stream(env, config, Kemal.config, decrypt_function, topics, connection_channel) + create_notification_stream(env, topics, connection_channel) end post "/api/v1/auth/notifications" do |env| @@ -4670,7 +4659,7 @@ post "/api/v1/auth/notifications" do |env| topics = env.params.body["topics"]?.try &.split(",").uniq.first(1000) topics ||= [] of String - create_notification_stream(env, config, Kemal.config, decrypt_function, topics, connection_channel) + create_notification_stream(env, topics, connection_channel) end get "/api/v1/auth/preferences" do |env| @@ -4714,7 +4703,7 @@ get "/api/v1/auth/feed" do |env| json.field "notifications" do json.array do notifications.each do |video| - video.to_json(locale, config, Kemal.config, json) + video.to_json(locale, json) end end end @@ -4722,7 +4711,7 @@ get "/api/v1/auth/feed" do |env| json.field "videos" do json.array do videos.each do |video| - video.to_json(locale, config, Kemal.config, json) + video.to_json(locale, json) end end end @@ -4794,7 +4783,7 @@ get "/api/v1/auth/playlists" do |env| JSON.build do |json| json.array do playlists.each do |playlist| - playlist.to_json(0, locale, config, Kemal.config, json) + playlist.to_json(0, locale, json) end end end @@ -4825,10 +4814,8 @@ post "/api/v1/auth/playlists" do |env| next error_message end - host_url = make_host_url(config, Kemal.config) - playlist = create_playlist(PG_DB, title, privacy, user) - env.response.headers["Location"] = "#{host_url}/api/v1/auth/playlists/#{playlist.id}" + env.response.headers["Location"] = "#{HOST_URL}/api/v1/auth/playlists/#{playlist.id}" env.response.status_code = 201 { "title" => title, @@ -4958,11 +4945,9 @@ post "/api/v1/auth/playlists/:plid/videos" do |env| PG_DB.exec("INSERT INTO playlist_videos VALUES (#{args})", args: video_array) PG_DB.exec("UPDATE playlists SET index = array_append(index, $1), video_count = video_count + 1, updated = $2 WHERE id = $3", playlist_video.index, Time.utc, plid) - host_url = make_host_url(config, Kemal.config) - - env.response.headers["Location"] = "#{host_url}/api/v1/auth/playlists/#{plid}/videos/#{playlist_video.index.to_u64.to_s(16).upcase}" + env.response.headers["Location"] = "#{HOST_URL}/api/v1/auth/playlists/#{plid}/videos/#{playlist_video.index.to_u64.to_s(16).upcase}" env.response.status_code = 201 - playlist_video.to_json(locale, config, Kemal.config, index: playlist.index.size) + playlist_video.to_json(locale, index: playlist.index.size) end delete "/api/v1/auth/playlists/:plid/videos/:index" do |env| @@ -5251,12 +5236,10 @@ 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(config, Kemal.config) - - manifest = manifest.body + manifest = response.body if local - manifest = manifest.gsub("https://www.youtube.com", host_url) + manifest = manifest.gsub("https://www.youtube.com", HOST_URL) manifest = manifest.gsub("index.m3u8", "index.m3u8?local=true") end @@ -5276,9 +5259,7 @@ get "/api/manifest/hls_playlist/*" do |env| env.response.content_type = "application/x-mpegURL" env.response.headers.add("Access-Control-Allow-Origin", "*") - host_url = make_host_url(config, Kemal.config) - - manifest = manifest.body + manifest = response.body if local manifest = manifest.gsub(/^https:\/\/r\d---.{11}\.c\.youtube\.com[^\n]*/m) do |match| @@ -5313,7 +5294,7 @@ get "/api/manifest/hls_playlist/*" do |env| raw_params["local"] = "true" - "#{host_url}/videoplayback?#{raw_params}" + "#{HOST_URL}/videoplayback?#{raw_params}" end end @@ -5784,7 +5765,7 @@ get "/vi/:id/:name" do |env| headers = HTTP::Headers{":authority" => "i.ytimg.com"} if name == "maxres.jpg" - build_thumbnails(id, config, Kemal.config).each do |thumb| + build_thumbnails(id).each do |thumb| if YT_POOL.client &.head("/vi/#{id}/#{thumb[:url]}.jpg", headers).status_code == 200 name = thumb[:url] + ".jpg" break diff --git a/src/invidious/channels.cr b/src/invidious/channels.cr index afc1528e..f1a57eee 100644 --- a/src/invidious/channels.cr +++ b/src/invidious/channels.cr @@ -9,14 +9,14 @@ struct InvidiousChannel end struct ChannelVideo - def to_json(locale, config, kemal_config, json : JSON::Builder) + def to_json(locale, json : JSON::Builder) json.object do json.field "type", "shortVideo" json.field "title", self.title json.field "videoId", self.id json.field "videoThumbnails" do - generate_thumbnails(json, self.id, config, Kemal.config) + generate_thumbnails(json, self.id) end json.field "lengthSeconds", self.length_seconds @@ -31,17 +31,17 @@ struct ChannelVideo end end - def to_json(locale, config, kemal_config, json : JSON::Builder | Nil = nil) + def to_json(locale, json : JSON::Builder | Nil = nil) if json - to_json(locale, config, kemal_config, json) + to_json(locale, json) else JSON.build do |json| - to_json(locale, config, kemal_config, json) + to_json(locale, json) end end end - def to_xml(locale, host_url, query_params, xml : XML::Builder) + def to_xml(locale, query_params, xml : XML::Builder) query_params["v"] = self.id xml.element("entry") do @@ -49,17 +49,17 @@ struct ChannelVideo xml.element("yt:videoId") { xml.text self.id } xml.element("yt:channelId") { xml.text self.ucid } xml.element("title") { xml.text self.title } - xml.element("link", rel: "alternate", href: "#{host_url}/watch?#{query_params}") + xml.element("link", rel: "alternate", href: "#{HOST_URL}/watch?#{query_params}") xml.element("author") do xml.element("name") { xml.text self.author } - xml.element("uri") { xml.text "#{host_url}/channel/#{self.ucid}" } + xml.element("uri") { xml.text "#{HOST_URL}/channel/#{self.ucid}" } end xml.element("content", type: "xhtml") do xml.element("div", xmlns: "http://www.w3.org/1999/xhtml") do - xml.element("a", href: "#{host_url}/watch?#{query_params}") do - xml.element("img", src: "#{host_url}/vi/#{self.id}/mqdefault.jpg") + xml.element("a", href: "#{HOST_URL}/watch?#{query_params}") do + xml.element("img", src: "#{HOST_URL}/vi/#{self.id}/mqdefault.jpg") end end end @@ -69,18 +69,18 @@ struct ChannelVideo xml.element("media:group") do xml.element("media:title") { xml.text self.title } - xml.element("media:thumbnail", url: "#{host_url}/vi/#{self.id}/mqdefault.jpg", + xml.element("media:thumbnail", url: "#{HOST_URL}/vi/#{self.id}/mqdefault.jpg", width: "320", height: "180") end end end - def to_xml(locale, config, kemal_config, xml : XML::Builder | Nil = nil) + def to_xml(locale, xml : XML::Builder | Nil = nil) if xml - to_xml(locale, config, kemal_config, xml) + to_xml(locale, xml) else XML.build do |xml| - to_xml(locale, config, kemal_config, xml) + to_xml(locale, xml) end end end @@ -557,7 +557,7 @@ def extract_channel_playlists_cursor(url, auto_generated) end # TODO: Add "sort_by" -def fetch_channel_community(ucid, continuation, locale, config, kemal_config, format, thin_mode) +def fetch_channel_community(ucid, continuation, locale, format, thin_mode) response = YT_POOL.client &.get("/channel/#{ucid}/community?gl=US&hl=en") if response.status_code != 200 response = YT_POOL.client &.get("/user/#{ucid}/community?gl=US&hl=en") @@ -708,7 +708,7 @@ def fetch_channel_community(ucid, continuation, locale, config, kemal_config, fo json.field "title", attachment["title"]["simpleText"].as_s json.field "videoId", video_id json.field "videoThumbnails" do - generate_thumbnails(json, video_id, config, kemal_config) + generate_thumbnails(json, video_id) end json.field "lengthSeconds", decode_length_seconds(attachment["lengthText"]["simpleText"].as_s) @@ -956,33 +956,17 @@ def get_about_info(ucid, locale) end def get_60_videos(ucid, author, page, auto_generated, sort_by = "newest") - count = 0 videos = [] of SearchVideo 2.times do |i| url = produce_channel_videos_url(ucid, page * 2 + (i - 1), auto_generated: auto_generated, sort_by: sort_by) - response = YT_POOL.client &.get(url) - json = JSON.parse(response.body) - - if json["content_html"]? && !json["content_html"].as_s.empty? - document = XML.parse_html(json["content_html"].as_s) - nodeset = document.xpath_nodes(%q(//li[contains(@class, "feed-item-container")])) - - if !json["load_more_widget_html"]?.try &.as_s.empty? - count += 30 - end - - if auto_generated - videos += extract_videos(nodeset) - else - videos += extract_videos(nodeset, ucid, author) - end - else - break - end + response = YT_POOL.client &.get(url, headers) + initial_data = JSON.parse(response.body).as_a.find &.["response"]? + break if !initial_data + videos.concat extract_videos(initial_data.as_h) end - return videos, count + return videos.size, videos end def get_latest_videos(ucid) diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr index f6ba33cd..b572ee1c 100644 --- a/src/invidious/helpers/helpers.cr +++ b/src/invidious/helpers/helpers.cr @@ -727,13 +727,10 @@ def cache_annotation(db, id, annotations) end end - if has_legacy_annotations - # TODO: Update on conflict? - db.exec("INSERT INTO annotations VALUES ($1, $2) ON CONFLICT DO NOTHING", id, annotations) - end + db.exec("INSERT INTO annotations VALUES ($1, $2) ON CONFLICT DO NOTHING", id, annotations) if has_legacy_annotations end -def create_notification_stream(env, config, kemal_config, decrypt_function, topics, connection_channel) +def create_notification_stream(env, topics, connection_channel) connection = Channel(PQ::Notification).new(8) connection_channel.send({true, connection}) @@ -753,7 +750,7 @@ def create_notification_stream(env, config, kemal_config, decrypt_function, topi video = get_video(video_id, PG_DB) video.published = published - response = JSON.parse(video.to_json(locale, config, kemal_config, decrypt_function)) + response = JSON.parse(video.to_json(locale)) if fields_text = env.params.query["fields"]? begin @@ -787,7 +784,7 @@ def create_notification_stream(env, config, kemal_config, decrypt_function, topi when .match(/UC[A-Za-z0-9_-]{22}/) PG_DB.query_all("SELECT * FROM channel_videos WHERE ucid = $1 AND published > $2 ORDER BY published DESC LIMIT 15", topic, Time.unix(since.not_nil!), as: ChannelVideo).each do |video| - response = JSON.parse(video.to_json(locale, config, Kemal.config)) + response = JSON.parse(video.to_json(locale)) if fields_text = env.params.query["fields"]? begin @@ -829,7 +826,7 @@ def create_notification_stream(env, config, kemal_config, decrypt_function, topi video = get_video(video_id, PG_DB) video.published = Time.unix(published) - response = JSON.parse(video.to_json(locale, config, Kemal.config, decrypt_function)) + response = JSON.parse(video.to_json(locale)) if fields_text = env.params.query["fields"]? begin diff --git a/src/invidious/helpers/signatures.cr b/src/invidious/helpers/signatures.cr index f82cc8dd..0aaacd04 100644 --- a/src/invidious/helpers/signatures.cr +++ b/src/invidious/helpers/signatures.cr @@ -40,12 +40,12 @@ def fetch_decrypt_function(id = "CvFH_6DNRCY") return decrypt_function end -def decrypt_signature(fmt, op) +def decrypt_signature(fmt : Hash(String, JSON::Any)) return "" if !fmt["s"]? || !fmt["sp"]? - sp = fmt["sp"] - sig = fmt["s"].split("") - op.each do |proc, value| + sp = fmt["sp"].as_s + sig = fmt["s"].as_s.split("") + DECRYPT_FUNCTION.each do |proc, value| sig = proc.call(sig, value) end diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index a4bd1d54..a39a0b16 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -351,10 +351,8 @@ def subscribe_pubsub(topic, key, config) nonce = Random::Secure.hex(4) signature = "#{time}:#{nonce}" - host_url = make_host_url(config, Kemal.config) - body = { - "hub.callback" => "#{host_url}/feed/webhook/v1:#{time}:#{nonce}:#{OpenSSL::HMAC.hexdigest(:sha1, key, signature)}", + "hub.callback" => "#{HOST_URL}/feed/webhook/v1:#{time}:#{nonce}:#{OpenSSL::HMAC.hexdigest(:sha1, key, signature)}", "hub.topic" => "https://www.youtube.com/xml/feeds/videos.xml?#{topic}", "hub.verify" => "async", "hub.mode" => "subscribe", diff --git a/src/invidious/playlists.cr b/src/invidious/playlists.cr index 184329dc..fcf73dad 100644 --- a/src/invidious/playlists.cr +++ b/src/invidious/playlists.cr @@ -1,26 +1,26 @@ struct PlaylistVideo - def to_xml(host_url, auto_generated, xml : XML::Builder) + def to_xml(auto_generated, xml : XML::Builder) xml.element("entry") do xml.element("id") { xml.text "yt:video:#{self.id}" } xml.element("yt:videoId") { xml.text self.id } xml.element("yt:channelId") { xml.text self.ucid } xml.element("title") { xml.text self.title } - xml.element("link", rel: "alternate", href: "#{host_url}/watch?v=#{self.id}") + xml.element("link", rel: "alternate", href: "#{HOST_URL}/watch?v=#{self.id}") xml.element("author") do if auto_generated xml.element("name") { xml.text self.author } - xml.element("uri") { xml.text "#{host_url}/channel/#{self.ucid}" } + xml.element("uri") { xml.text "#{HOST_URL}/channel/#{self.ucid}" } else xml.element("name") { xml.text author } - xml.element("uri") { xml.text "#{host_url}/channel/#{ucid}" } + xml.element("uri") { xml.text "#{HOST_URL}/channel/#{ucid}" } end end xml.element("content", type: "xhtml") do xml.element("div", xmlns: "http://www.w3.org/1999/xhtml") do - xml.element("a", href: "#{host_url}/watch?v=#{self.id}") do - xml.element("img", src: "#{host_url}/vi/#{self.id}/mqdefault.jpg") + xml.element("a", href: "#{HOST_URL}/watch?v=#{self.id}") do + xml.element("img", src: "#{HOST_URL}/vi/#{self.id}/mqdefault.jpg") end end end @@ -29,23 +29,23 @@ struct PlaylistVideo xml.element("media:group") do xml.element("media:title") { xml.text self.title } - xml.element("media:thumbnail", url: "#{host_url}/vi/#{self.id}/mqdefault.jpg", + xml.element("media:thumbnail", url: "#{HOST_URL}/vi/#{self.id}/mqdefault.jpg", width: "320", height: "180") end end end - def to_xml(host_url, auto_generated, xml : XML::Builder? = nil) + def to_xml(auto_generated, xml : XML::Builder? = nil) if xml - to_xml(host_url, auto_generated, xml) + to_xml(auto_generated, xml) else XML.build do |json| - to_xml(host_url, auto_generated, xml) + to_xml(auto_generated, xml) end end end - def to_json(locale, config, kemal_config, json : JSON::Builder, index : Int32?) + def to_json(locale, json : JSON::Builder, index : Int32?) json.object do json.field "title", self.title json.field "videoId", self.id @@ -55,7 +55,7 @@ struct PlaylistVideo json.field "authorUrl", "/channel/#{self.ucid}" json.field "videoThumbnails" do - generate_thumbnails(json, self.id, config, kemal_config) + generate_thumbnails(json, self.id) end if index @@ -69,12 +69,12 @@ struct PlaylistVideo end end - def to_json(locale, config, kemal_config, json : JSON::Builder? = nil, index : Int32? = nil) + def to_json(locale, json : JSON::Builder? = nil, index : Int32? = nil) if json - to_json(locale, config, kemal_config, json, index: index) + to_json(locale, json, index: index) else JSON.build do |json| - to_json(locale, config, kemal_config, json, index: index) + to_json(locale, json, index: index) end end end @@ -93,7 +93,7 @@ struct PlaylistVideo end struct Playlist - def to_json(offset, locale, config, kemal_config, json : JSON::Builder, continuation : String? = nil) + def to_json(offset, locale, json : JSON::Builder, continuation : String? = nil) json.object do json.field "type", "playlist" json.field "title", self.title @@ -130,19 +130,19 @@ struct Playlist json.array do videos = get_playlist_videos(PG_DB, self, offset: offset, locale: locale, continuation: continuation) videos.each_with_index do |video, index| - video.to_json(locale, config, Kemal.config, json) + video.to_json(locale, json) end end end end end - def to_json(offset, locale, config, kemal_config, json : JSON::Builder? = nil, continuation : String? = nil) + def to_json(offset, locale, json : JSON::Builder? = nil, continuation : String? = nil) if json - to_json(offset, locale, config, kemal_config, json, continuation: continuation) + to_json(offset, locale, json, continuation: continuation) else JSON.build do |json| - to_json(offset, locale, config, kemal_config, json, continuation: continuation) + to_json(offset, locale, json, continuation: continuation) end end end @@ -172,7 +172,7 @@ enum PlaylistPrivacy end struct InvidiousPlaylist - def to_json(offset, locale, config, kemal_config, json : JSON::Builder, continuation : String? = nil) + def to_json(offset, locale, json : JSON::Builder, continuation : String? = nil) json.object do json.field "type", "invidiousPlaylist" json.field "title", self.title @@ -195,19 +195,19 @@ struct InvidiousPlaylist json.array do videos = get_playlist_videos(PG_DB, self, offset: offset, locale: locale, continuation: continuation) videos.each_with_index do |video, index| - video.to_json(locale, config, Kemal.config, json, offset + index) + video.to_json(locale, json, offset + index) end end end end end - def to_json(offset, locale, config, kemal_config, json : JSON::Builder? = nil, continuation : String? = nil) + def to_json(offset, locale, json : JSON::Builder? = nil, continuation : String? = nil) if json - to_json(offset, locale, config, kemal_config, json, continuation: continuation) + to_json(offset, locale, json, continuation: continuation) else JSON.build do |json| - to_json(offset, locale, config, kemal_config, json, continuation: continuation) + to_json(offset, locale, json, continuation: continuation) end end end diff --git a/src/invidious/search.cr b/src/invidious/search.cr index e8521629..7a88f316 100644 --- a/src/invidious/search.cr +++ b/src/invidious/search.cr @@ -1,5 +1,5 @@ struct SearchVideo - def to_xml(host_url, auto_generated, query_params, xml : XML::Builder) + def to_xml(auto_generated, query_params, xml : XML::Builder) query_params["v"] = self.id xml.element("entry") do @@ -7,22 +7,22 @@ struct SearchVideo xml.element("yt:videoId") { xml.text self.id } xml.element("yt:channelId") { xml.text self.ucid } xml.element("title") { xml.text self.title } - xml.element("link", rel: "alternate", href: "#{host_url}/watch?#{query_params}") + xml.element("link", rel: "alternate", href: "#{HOST_URL}/watch?#{query_params}") xml.element("author") do if auto_generated xml.element("name") { xml.text self.author } - xml.element("uri") { xml.text "#{host_url}/channel/#{self.ucid}" } + xml.element("uri") { xml.text "#{HOST_URL}/channel/#{self.ucid}" } else xml.element("name") { xml.text author } - xml.element("uri") { xml.text "#{host_url}/channel/#{ucid}" } + xml.element("uri") { xml.text "#{HOST_URL}/channel/#{ucid}" } end end xml.element("content", type: "xhtml") do xml.element("div", xmlns: "http://www.w3.org/1999/xhtml") do - xml.element("a", href: "#{host_url}/watch?#{query_params}") do - xml.element("img", src: "#{host_url}/vi/#{self.id}/mqdefault.jpg") + xml.element("a", href: "#{HOST_URL}/watch?#{query_params}") do + xml.element("img", src: "#{HOST_URL}/vi/#{self.id}/mqdefault.jpg") end xml.element("p", style: "word-break:break-word;white-space:pre-wrap") { xml.text html_to_content(self.description_html) } @@ -33,7 +33,7 @@ struct SearchVideo xml.element("media:group") do xml.element("media:title") { xml.text self.title } - xml.element("media:thumbnail", url: "#{host_url}/vi/#{self.id}/mqdefault.jpg", + xml.element("media:thumbnail", url: "#{HOST_URL}/vi/#{self.id}/mqdefault.jpg", width: "320", height: "180") xml.element("media:description") { xml.text html_to_content(self.description_html) } end @@ -44,17 +44,17 @@ struct SearchVideo end end - def to_xml(host_url, auto_generated, query_params, xml : XML::Builder | Nil = nil) + def to_xml(auto_generated, query_params, xml : XML::Builder | Nil = nil) if xml - to_xml(host_url, auto_generated, query_params, xml) + to_xml(HOST_URL, auto_generated, query_params, xml) else XML.build do |json| - to_xml(host_url, auto_generated, query_params, xml) + to_xml(HOST_URL, auto_generated, query_params, xml) end end end - def to_json(locale, config, kemal_config, json : JSON::Builder) + def to_json(locale, json : JSON::Builder) json.object do json.field "type", "video" json.field "title", self.title @@ -65,7 +65,7 @@ struct SearchVideo json.field "authorUrl", "/channel/#{self.ucid}" json.field "videoThumbnails" do - generate_thumbnails(json, self.id, config, kemal_config) + generate_thumbnails(json, self.id) end json.field "description", html_to_content(self.description_html) @@ -78,15 +78,20 @@ struct SearchVideo json.field "liveNow", self.live_now json.field "paid", self.paid json.field "premium", self.premium + json.field "isUpcoming", self.is_upcoming + + if self.premiere_timestamp + json.field "premiereTimestamp", self.premiere_timestamp.try &.to_unix + end end end - def to_json(locale, config, kemal_config, json : JSON::Builder | Nil = nil) + def to_json(locale, json : JSON::Builder | Nil = nil) if json - to_json(locale, config, kemal_config, json) + to_json(locale, json) else JSON.build do |json| - to_json(locale, config, kemal_config, json) + to_json(locale, json) end end end @@ -116,7 +121,7 @@ struct SearchPlaylistVideo end struct SearchPlaylist - def to_json(locale, config, kemal_config, json : JSON::Builder) + def to_json(locale, json : JSON::Builder) json.object do json.field "type", "playlist" json.field "title", self.title @@ -137,7 +142,7 @@ struct SearchPlaylist json.field "lengthSeconds", video.length_seconds json.field "videoThumbnails" do - generate_thumbnails(json, video.id, config, Kemal.config) + generate_thumbnails(json, video.id) end end end @@ -146,12 +151,12 @@ struct SearchPlaylist end end - def to_json(locale, config, kemal_config, json : JSON::Builder | Nil = nil) + def to_json(locale, json : JSON::Builder | Nil = nil) if json - to_json(locale, config, kemal_config, json) + to_json(locale, json) else JSON.build do |json| - to_json(locale, config, kemal_config, json) + to_json(locale, json) end end end @@ -168,7 +173,7 @@ struct SearchPlaylist end struct SearchChannel - def to_json(locale, config, kemal_config, json : JSON::Builder) + def to_json(locale, json : JSON::Builder) json.object do json.field "type", "channel" json.field "author", self.author @@ -198,12 +203,12 @@ struct SearchChannel end end - def to_json(locale, config, kemal_config, json : JSON::Builder | Nil = nil) + def to_json(locale, json : JSON::Builder | Nil = nil) if json - to_json(locale, config, kemal_config, json) + to_json(locale, json) else JSON.build do |json| - to_json(locale, config, kemal_config, json) + to_json(locale, json) end end end diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index 7e815ca1..ed5847e4 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -255,17 +255,20 @@ struct Video end end - def to_json(locale, config, kemal_config, decrypt_function, json : JSON::Builder) + def to_json(locale, json : JSON::Builder) json.object do json.field "type", "video" json.field "title", self.title json.field "videoId", self.id + + json.field "error", info["reason"] if info["reason"]? + json.field "videoThumbnails" do - generate_thumbnails(json, self.id, config, kemal_config) + generate_thumbnails(json, self.id) end json.field "storyboards" do - generate_storyboards(json, self.id, self.storyboards, config, kemal_config) + generate_storyboards(json, self.id, self.storyboards) end json.field "description", html_to_content(self.description_html) @@ -316,16 +319,12 @@ struct Video json.field "premiereTimestamp", self.premiere_timestamp.not_nil!.to_unix end - if player_response["streamingData"]?.try &.["hlsManifestUrl"]? - host_url = make_host_url(config, kemal_config) - - hlsvp = player_response["streamingData"]["hlsManifestUrl"].as_s - hlsvp = hlsvp.gsub("https://manifest.googlevideo.com", host_url) - + if hlsvp = self.hls_manifest_url + hlsvp = hlsvp.gsub("https://manifest.googlevideo.com", HOST_URL) json.field "hlsUrl", hlsvp end - json.field "dashUrl", "#{make_host_url(config, kemal_config)}/api/manifest/dash/id/#{id}" + json.field "dashUrl", "#{HOST_URL}/api/manifest/dash/id/#{id}" json.field "adaptiveFormats" do json.array do @@ -424,7 +423,7 @@ struct Video json.field "videoId", rv["id"] json.field "title", rv["title"] json.field "videoThumbnails" do - generate_thumbnails(json, rv["id"], config, kemal_config) + generate_thumbnails(json, rv["id"]) end json.field "author", rv["author"] @@ -457,12 +456,12 @@ struct Video end end - def to_json(locale, config, kemal_config, decrypt_function, json : JSON::Builder | Nil = nil) + def to_json(locale, json : JSON::Builder | Nil = nil) if json - to_json(locale, config, kemal_config, decrypt_function, json) + to_json(locale, json) else JSON.build do |json| - to_json(locale, config, kemal_config, decrypt_function, json) + to_json(locale, json) end end end @@ -1391,9 +1390,9 @@ def process_video_params(query, preferences) return params end -def build_thumbnails(id, config, kemal_config) +def build_thumbnails(id) return { - {name: "maxres", host: "#{make_host_url(config, kemal_config)}", url: "maxres", height: 720, width: 1280}, + {name: "maxres", host: "#{HOST_URL}", url: "maxres", height: 720, width: 1280}, {name: "maxresdefault", host: "https://i.ytimg.com", url: "maxresdefault", height: 720, width: 1280}, {name: "sddefault", host: "https://i.ytimg.com", url: "sddefault", height: 480, width: 640}, {name: "high", host: "https://i.ytimg.com", url: "hqdefault", height: 360, width: 480}, @@ -1405,9 +1404,9 @@ def build_thumbnails(id, config, kemal_config) } end -def generate_thumbnails(json, id, config, kemal_config) +def generate_thumbnails(json, id) json.array do - build_thumbnails(id, config, kemal_config).each do |thumbnail| + build_thumbnails(id).each do |thumbnail| json.object do json.field "quality", thumbnail[:name] json.field "url", "#{thumbnail[:host]}/vi/#{id}/#{thumbnail["url"]}.jpg" @@ -1418,7 +1417,7 @@ def generate_thumbnails(json, id, config, kemal_config) end end -def generate_storyboards(json, id, storyboards, config, kemal_config) +def generate_storyboards(json, id, storyboards) json.array do storyboards.each do |storyboard| json.object do diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 708456f9..ae6341e0 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -3,23 +3,23 @@ "> - + - + - - + + - + - - - + + + <%= rendered "components/player_sources" %> From 1eca969cf6b4096789014619285c98d1def40ee3 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Mon, 15 Jun 2020 17:33:23 -0500 Subject: [PATCH 0124/2930] Add support for polymer redesign --- config/sql/videos.sql | 17 - spec/helpers_spec.cr | 4 +- src/invidious.cr | 173 ++-- src/invidious/channels.cr | 32 +- src/invidious/comments.cr | 24 +- src/invidious/helpers/helpers.cr | 168 +++- src/invidious/helpers/jobs.cr | 2 +- src/invidious/helpers/signatures.cr | 4 +- src/invidious/helpers/utils.cr | 2 +- src/invidious/mixes.cr | 1 - src/invidious/search.cr | 69 +- src/invidious/trending.cr | 24 +- src/invidious/users.cr | 4 +- src/invidious/videos.cr | 926 ++++++++-------------- src/invidious/views/components/item.ecr | 4 +- src/invidious/views/components/player.ecr | 12 +- src/invidious/views/watch.ecr | 54 +- 17 files changed, 634 insertions(+), 886 deletions(-) diff --git a/config/sql/videos.sql b/config/sql/videos.sql index 6ded01de..8def2f83 100644 --- a/config/sql/videos.sql +++ b/config/sql/videos.sql @@ -7,23 +7,6 @@ CREATE TABLE public.videos id text NOT NULL, info text, updated timestamp with time zone, - title text, - views bigint, - likes integer, - dislikes integer, - wilson_score double precision, - published timestamp with time zone, - description text, - language text, - author text, - ucid text, - allowed_regions text[], - is_family_friendly boolean, - genre text, - genre_url text, - license text, - sub_count_text text, - author_thumbnail text, CONSTRAINT videos_pkey PRIMARY KEY (id) ); diff --git a/spec/helpers_spec.cr b/spec/helpers_spec.cr index 37e36c61..26922bb2 100644 --- a/spec/helpers_spec.cr +++ b/spec/helpers_spec.cr @@ -27,9 +27,9 @@ describe "Helper" do describe "#produce_channel_search_url" do it "correctly produces token for searching a specific channel" do - produce_channel_search_url("UCXuqSBlHAE6Xw-yeJA0Tunw", "", 100).should eq("/browse_ajax?continuation=4qmFsgI-EhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaIEVnWnpaV0Z5WTJnd0FqZ0JZQUZxQUxnQkFIb0RNVEF3WgA%3D&gl=US&hl=en") + produce_channel_search_url("UCXuqSBlHAE6Xw-yeJA0Tunw", "", 100).should eq("/browse_ajax?continuation=4qmFsgI2EhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaGEVnWnpaV0Z5WTJnNEFYb0RNVEF3dUFFQVoA&gl=US&hl=en") - produce_channel_search_url("UCXuqSBlHAE6Xw-yeJA0Tunw", "По ожиशुपतिरपि子而時ஸ்றீனி", 0).should eq("/browse_ajax?continuation=4qmFsgJ8EhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaIEVnWnpaV0Z5WTJnd0FqZ0JZQUZxQUxnQkFIb0JNQT09Wj7Qn9C-INC-0LbQuOCktuClgeCkquCkpOCkv-CksOCkquCkv-WtkOiAjOaZguCuuOCvjeCuseCvgOCuqeCuvw%3D%3D&gl=US&hl=en") + produce_channel_search_url("UCXuqSBlHAE6Xw-yeJA0Tunw", "По ожиशुपतिरपि子而時ஸ்றீனி", 0).should eq("/browse_ajax?continuation=4qmFsgJ0EhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaGEVnWnpaV0Z5WTJnNEFYb0JNTGdCQUE9PVo-0J_QviDQvtC20LjgpLbgpYHgpKrgpKTgpL_gpLDgpKrgpL_lrZDogIzmmYLgrrjgr43grrHgr4Dgrqngrr8%3D&gl=US&hl=en") end end diff --git a/src/invidious.cr b/src/invidious.cr index 958f95f7..c95c6419 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -510,16 +510,16 @@ get "/watch" do |env| comment_html ||= "" end - fmt_stream = video.fmt_stream(decrypt_function) - adaptive_fmts = video.adaptive_fmts(decrypt_function) + fmt_stream = video.fmt_stream + adaptive_fmts = video.adaptive_fmts if params.local - fmt_stream.each { |fmt| fmt["url"] = URI.parse(fmt["url"]).full_path } - adaptive_fmts.each { |fmt| fmt["url"] = URI.parse(fmt["url"]).full_path } + fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).full_path) } + adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).full_path) } end - video_streams = video.video_streams(adaptive_fmts) - audio_streams = video.audio_streams(adaptive_fmts) + video_streams = video.video_streams + audio_streams = video.audio_streams # Older videos may not have audio sources available. # We redirect here so they're not unplayable @@ -549,33 +549,23 @@ get "/watch" do |env| aspect_ratio = "16:9" - video.description_html = fill_links(video.description_html, "https", "www.youtube.com") - video.description_html = replace_links(video.description_html) - - host_url = make_host_url(config, Kemal.config) - - if video.player_response["streamingData"]?.try &.["hlsManifestUrl"]? - hlsvp = video.player_response["streamingData"]["hlsManifestUrl"].as_s - hlsvp = hlsvp.gsub("https://manifest.googlevideo.com", host_url) - end - thumbnail = "/vi/#{video.id}/maxres.jpg" if params.raw if params.listen - url = audio_streams[0]["url"] + url = audio_streams[0]["url"].as_s audio_streams.each do |fmt| - if fmt["bitrate"] == params.quality.rchop("k") - url = fmt["url"] + if fmt["bitrate"].as_i == params.quality.rchop("k").to_i + url = fmt["url"].as_s end end else - url = fmt_stream[0]["url"] + url = fmt_stream[0]["url"].as_s fmt_stream.each do |fmt| - if fmt["label"].split(" - ")[0] == params.quality - url = fmt["url"] + if fmt["quality"].as_s == params.quality + url = fmt["url"].as_s end end end @@ -583,24 +573,6 @@ get "/watch" do |env| next env.redirect url end - rvs = [] of Hash(String, String) - video.info["rvs"]?.try &.split(",").each do |rv| - rvs << HTTP::Params.parse(rv).to_h - end - - rating = video.info["avg_rating"].to_f64 - if video.views > 0 - engagement = ((video.dislikes.to_f + video.likes.to_f)/video.views * 100) - else - engagement = 0 - end - - playability_status = video.player_response["playabilityStatus"]? - if playability_status && playability_status["status"] == "LIVE_STREAM_OFFLINE" && !video.premiere_timestamp - reason = playability_status["reason"]?.try &.as_s - end - reason ||= "" - templated "watch" end @@ -752,16 +724,16 @@ get "/embed/:id" do |env| notifications.delete(id) end - fmt_stream = video.fmt_stream(decrypt_function) - adaptive_fmts = video.adaptive_fmts(decrypt_function) + fmt_stream = video.fmt_stream + adaptive_fmts = video.adaptive_fmts if params.local - fmt_stream.each { |fmt| fmt["url"] = URI.parse(fmt["url"]).full_path } - adaptive_fmts.each { |fmt| fmt["url"] = URI.parse(fmt["url"]).full_path } + fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).full_path) } + adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).full_path) } end - video_streams = video.video_streams(adaptive_fmts) - audio_streams = video.audio_streams(adaptive_fmts) + video_streams = video.video_streams + audio_streams = video.audio_streams if audio_streams.empty? && !video.live_now if params.quality == "dash" @@ -788,25 +760,13 @@ get "/embed/:id" do |env| aspect_ratio = nil - video.description_html = fill_links(video.description_html, "https", "www.youtube.com") - video.description_html = replace_links(video.description_html) - - host_url = make_host_url(config, Kemal.config) - - if video.player_response["streamingData"]?.try &.["hlsManifestUrl"]? - hlsvp = video.player_response["streamingData"]["hlsManifestUrl"].as_s - hlsvp = hlsvp.gsub("https://manifest.googlevideo.com", host_url) - end - thumbnail = "/vi/#{video.id}/maxres.jpg" if params.raw - url = fmt_stream[0]["url"] + url = fmt_stream[0]["url"].as_s fmt_stream.each do |fmt| - if fmt["label"].split(" - ")[0] == params.quality - url = fmt["url"] - end + url = fmt["url"].as_s if fmt["quality"].as_s == params.quality end next env.redirect url @@ -1469,7 +1429,6 @@ post "/login" do |env| traceback = IO::Memory.new # See https://github.com/ytdl-org/youtube-dl/blob/2019.04.07/youtube_dl/extractor/youtube.py#L82 - # TODO: Convert to QUIC begin client = QUIC::Client.new(LOGIN_URL) headers = HTTP::Headers.new @@ -2329,8 +2288,7 @@ get "/modify_notifications" do |env| end headers = cookies.add_request_headers(headers) - match = html.body.match(/'XSRF_TOKEN': "(?[A-Za-z0-9\_\-\=]+)"/) - if match + if match = html.body.match(/'XSRF_TOKEN': "(?[^"]+)"/) session_token = match["session_token"] else next env.redirect referer @@ -3575,14 +3533,14 @@ get "/channel/:ucid" do |env| item.author end end - items = items.select { |item| item.is_a?(SearchPlaylist) }.map { |item| item.as(SearchPlaylist) } + items = items.select(&.is_a?(SearchPlaylist)).map(&.as(SearchPlaylist)) items.each { |item| item.author = "" } else sort_options = {"newest", "oldest", "popular"} sort_by ||= "newest" - items, count = get_60_videos(channel.ucid, channel.author, page, channel.auto_generated, sort_by) - items.select! { |item| !item.paid } + count, items = get_60_videos(channel.ucid, channel.author, page, channel.auto_generated, sort_by) + items.reject! &.paid env.set "search", "channel:#{channel.ucid} " end @@ -5125,7 +5083,7 @@ get "/api/manifest/dash/id/:id" do |env| next end - if dashmpd = video.player_response["streamingData"]?.try &.["dashManifestUrl"]?.try &.as_s + if dashmpd = video.dash_manifest_url manifest = YT_POOL.client &.get(URI.parse(dashmpd).full_path).body manifest = manifest.gsub(/[^<]+<\/BaseURL>/) do |baseurl| @@ -5142,16 +5100,16 @@ get "/api/manifest/dash/id/:id" do |env| next manifest end - adaptive_fmts = video.adaptive_fmts(decrypt_function) + adaptive_fmts = video.adaptive_fmts if local adaptive_fmts.each do |fmt| - fmt["url"] = URI.parse(fmt["url"]).full_path + fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).full_path) end end - audio_streams = video.audio_streams(adaptive_fmts) - video_streams = video.video_streams(adaptive_fmts).sort_by { |stream| {stream["size"].split("x")[0].to_i, stream["fps"].to_i} }.reverse + audio_streams = video.audio_streams + video_streams = video.video_streams.sort_by { |stream| {stream["width"].as_i, stream["fps"].as_i} }.reverse XML.build(indent: " ", encoding: "UTF-8") do |xml| xml.element("MPD", "xmlns": "urn:mpeg:dash:schema:mpd:2011", @@ -5161,24 +5119,22 @@ get "/api/manifest/dash/id/:id" do |env| i = 0 {"audio/mp4", "audio/webm"}.each do |mime_type| - mime_streams = audio_streams.select { |stream| stream["type"].starts_with? mime_type } - if mime_streams.empty? - next - end + mime_streams = audio_streams.select { |stream| stream["mimeType"].as_s.starts_with? mime_type } + next if mime_streams.empty? xml.element("AdaptationSet", id: i, mimeType: mime_type, startWithSAP: 1, subsegmentAlignment: true) do mime_streams.each do |fmt| - codecs = fmt["type"].split("codecs=")[1].strip('"') - bandwidth = fmt["bitrate"].to_i * 1000 - itag = fmt["itag"] - url = fmt["url"] + codecs = fmt["mimeType"].as_s.split("codecs=")[1].strip('"') + bandwidth = fmt["bitrate"].as_i + itag = fmt["itag"].as_i + url = fmt["url"].as_s xml.element("Representation", id: fmt["itag"], codecs: codecs, bandwidth: bandwidth) do xml.element("AudioChannelConfiguration", schemeIdUri: "urn:mpeg:dash:23003:3:audio_channel_configuration:2011", value: "2") xml.element("BaseURL") { xml.text url } - xml.element("SegmentBase", indexRange: fmt["index"]) do - xml.element("Initialization", range: fmt["init"]) + xml.element("SegmentBase", indexRange: "#{fmt["indexRange"]["start"]}-#{fmt["indexRange"]["end"]}") do + xml.element("Initialization", range: "#{fmt["initRange"]["start"]}-#{fmt["initRange"]["end"]}") end end end @@ -5187,21 +5143,24 @@ get "/api/manifest/dash/id/:id" do |env| i += 1 end + potential_heights = {4320, 2160, 1440, 1080, 720, 480, 360, 240, 144} + {"video/mp4", "video/webm"}.each do |mime_type| - mime_streams = video_streams.select { |stream| stream["type"].starts_with? mime_type } + mime_streams = video_streams.select { |stream| stream["mimeType"].as_s.starts_with? mime_type } next if mime_streams.empty? heights = [] of Int32 xml.element("AdaptationSet", id: i, mimeType: mime_type, startWithSAP: 1, subsegmentAlignment: true, scanType: "progressive") do mime_streams.each do |fmt| - codecs = fmt["type"].split("codecs=")[1].strip('"') - bandwidth = fmt["bitrate"] - itag = fmt["itag"] - url = fmt["url"] - width, height = fmt["size"].split("x").map { |i| i.to_i } + codecs = fmt["mimeType"].as_s.split("codecs=")[1].strip('"') + bandwidth = fmt["bitrate"].as_i + itag = fmt["itag"].as_i + url = fmt["url"].as_s + width = fmt["width"].as_i + height = fmt["height"].as_i # Resolutions reported by YouTube player (may not accurately reflect source) - height = [4320, 2160, 1440, 1080, 720, 480, 360, 240, 144].sort_by { |i| (height - i).abs }[0] + height = potential_heights.min_by { |i| (height - i).abs } next if unique_res && heights.includes? height heights << height @@ -5209,8 +5168,8 @@ get "/api/manifest/dash/id/:id" do |env| startWithSAP: "1", maxPlayoutRate: "1", bandwidth: bandwidth, frameRate: fmt["fps"]) do xml.element("BaseURL") { xml.text url } - xml.element("SegmentBase", indexRange: fmt["index"]) do - xml.element("Initialization", range: fmt["init"]) + xml.element("SegmentBase", indexRange: "#{fmt["indexRange"]["start"]}-#{fmt["indexRange"]["end"]}") do + xml.element("Initialization", range: "#{fmt["initRange"]["start"]}-#{fmt["initRange"]["end"]}") end end end @@ -5224,10 +5183,10 @@ get "/api/manifest/dash/id/:id" do |env| end get "/api/manifest/hls_variant/*" do |env| - manifest = YT_POOL.client &.get(env.request.path) + response = YT_POOL.client &.get(env.request.path) - if manifest.status_code != 200 - env.response.status_code = manifest.status_code + if response.status_code != 200 + env.response.status_code = response.status_code next end @@ -5247,10 +5206,10 @@ get "/api/manifest/hls_variant/*" do |env| end get "/api/manifest/hls_playlist/*" do |env| - manifest = YT_POOL.client &.get(env.request.path) + response = YT_POOL.client &.get(env.request.path) - if manifest.status_code != 200 - env.response.status_code = manifest.status_code + if response.status_code != 200 + env.response.status_code = response.status_code next end @@ -5320,7 +5279,7 @@ get "/latest_version" do |env| end id ||= env.params.query["id"]? - itag ||= env.params.query["itag"]? + itag ||= env.params.query["itag"]?.try &.to_i region = env.params.query["region"]? @@ -5335,26 +5294,16 @@ get "/latest_version" do |env| video = get_video(id, PG_DB, region: region) - fmt_stream = video.fmt_stream(decrypt_function) - adaptive_fmts = video.adaptive_fmts(decrypt_function) + fmt = video.fmt_stream.find(nil) { |f| f["itag"].as_i == itag } || video.adaptive_fmts.find(nil) { |f| f["itag"].as_i == itag } + url = fmt.try &.["url"]?.try &.as_s - urls = (fmt_stream + adaptive_fmts).select { |fmt| fmt["itag"] == itag } - if urls.empty? + if !url env.response.status_code = 404 next - elsif urls.size > 1 - env.response.status_code = 409 - next end - url = urls[0]["url"] - if local - url = URI.parse(url).full_path.not_nil! - end - - if title - url += "&title=#{title}" - end + url = URI.parse(url).full_path.not_nil! if local + url = "#{url}&title=#{title}" if title env.redirect url end diff --git a/src/invidious/channels.cr b/src/invidious/channels.cr index f1a57eee..cbfa521d 100644 --- a/src/invidious/channels.cr +++ b/src/invidious/channels.cr @@ -232,9 +232,9 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil) nodeset = document.xpath_nodes(%q(//li[contains(@class, "feed-item-container")])) if auto_generated - videos = extract_videos(nodeset) + videos = extract_videos_html(nodeset) else - videos = extract_videos(nodeset, ucid, author) + videos = extract_videos_html(nodeset, ucid, author) end end @@ -317,9 +317,9 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil) nodeset = nodeset.not_nil! if auto_generated - videos = extract_videos(nodeset) + videos = extract_videos_html(nodeset) else - videos = extract_videos(nodeset, ucid, author) + videos = extract_videos_html(nodeset, ucid, author) end count = nodeset.size @@ -429,7 +429,7 @@ def fetch_channel_playlists(ucid, author, auto_generated, continuation, sort_by) if auto_generated items = extract_shelf_items(nodeset, ucid, author) else - items = extract_items(nodeset, ucid, author) + items = extract_items_html(nodeset, ucid, author) end return items, continuation @@ -584,16 +584,8 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode) headers = HTTP::Headers.new headers["cookie"] = response.cookies.add_request_headers(headers)["cookie"] - headers["content-type"] = "application/x-www-form-urlencoded" - headers["x-client-data"] = "CIi2yQEIpbbJAQipncoBCNedygEIqKPKAQ==" - headers["x-spf-previous"] = "" - headers["x-spf-referer"] = "" - - headers["x-youtube-client-name"] = "1" - headers["x-youtube-client-version"] = "2.20180719" - - session_token = response.body.match(/"XSRF_TOKEN":"(?[A-Za-z0-9\_\-\=]+)"/).try &.["session_token"]? || "" + session_token = response.body.match(/"XSRF_TOKEN":"(?[^"]+)"/).try &.["session_token"]? || "" post_req = { session_token: session_token, } @@ -633,13 +625,7 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode) next if !post - if !post["contentText"]? - content_html = "" - else - content_html = post["contentText"]["simpleText"]?.try &.as_s.rchop('\ufeff').try { |b| HTML.escape(b) }.to_s || - post["contentText"]["runs"]?.try &.as_a.try { |r| content_to_comment_html(r).try &.to_s } || "" - end - + content_html = post["contentText"]?.try { |t| parse_content(t) } || "" author = post["authorText"]?.try &.["simpleText"]? || "" json.object do @@ -960,7 +946,7 @@ def get_60_videos(ucid, author, page, auto_generated, sort_by = "newest") 2.times do |i| url = produce_channel_videos_url(ucid, page * 2 + (i - 1), auto_generated: auto_generated, sort_by: sort_by) - response = YT_POOL.client &.get(url, headers) + response = YT_POOL.client &.get(url) initial_data = JSON.parse(response.body).as_a.find &.["response"]? break if !initial_data videos.concat extract_videos(initial_data.as_h) @@ -980,7 +966,7 @@ def get_latest_videos(ucid) document = XML.parse_html(json["content_html"].as_s) nodeset = document.xpath_nodes(%q(//li[contains(@class, "feed-item-container")])) - videos = extract_videos(nodeset, ucid) + videos = extract_videos_html(nodeset, ucid) end return videos diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr index 24564bb9..5490d2ea 100644 --- a/src/invidious/comments.cr +++ b/src/invidious/comments.cr @@ -59,7 +59,7 @@ end def fetch_youtube_comments(id, db, cursor, format, locale, thin_mode, region, sort_by = "top") video = get_video(id, db, region: region) - session_token = video.info["session_token"]? + session_token = video.session_token case cursor when nil, "" @@ -85,17 +85,9 @@ def fetch_youtube_comments(id, db, cursor, format, locale, thin_mode, region, so session_token: session_token, } - headers = HTTP::Headers.new - - headers["content-type"] = "application/x-www-form-urlencoded" - headers["cookie"] = video.info["cookie"] - - headers["x-client-data"] = "CIi2yQEIpbbJAQipncoBCNedygEIqKPKAQ==" - headers["x-spf-previous"] = "https://www.youtube.com/watch?v=#{id}&gl=US&hl=en&disable_polymer=1&has_verified=1&bpctr=9999999999" - headers["x-spf-referer"] = "https://www.youtube.com/watch?v=#{id}&gl=US&hl=en&disable_polymer=1&has_verified=1&bpctr=9999999999" - - headers["x-youtube-client-name"] = "1" - headers["x-youtube-client-version"] = "2.20180719" + headers = HTTP::Headers{ + "cookie" => video.cookie, + } response = YT_POOL.client(region, &.post("/comment_service_ajax?action_get_comments=1&hl=en&gl=US", headers, form: post_req)) response = JSON.parse(response.body) @@ -150,8 +142,7 @@ def fetch_youtube_comments(id, db, cursor, format, locale, thin_mode, region, so node_comment = node["commentRenderer"] end - content_html = node_comment["contentText"]["simpleText"]?.try &.as_s.rchop('\ufeff').try { |b| HTML.escape(b) }.to_s || - node_comment["contentText"]["runs"]?.try &.as_a.try { |r| content_to_comment_html(r).try &.to_s } || "" + content_html = node_comment["contentText"]?.try { |t| parse_content(t) } || "" author = node_comment["authorText"]?.try &.["simpleText"]? || "" json.field "author", author @@ -523,6 +514,11 @@ def fill_links(html, scheme, host) return html.to_xml(options: XML::SaveOptions::NO_DECL) end +def parse_content(content : JSON::Any) : String + content["simpleText"]?.try &.as_s.rchop('\ufeff').try { |b| HTML.escape(b) }.to_s || + content["runs"]?.try &.as_a.try { |r| content_to_comment_html(r).try &.to_s } || "" +end + def content_to_comment_html(content) comment_html = content.map do |run| text = HTML.escape(run["text"].as_s) diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr index b572ee1c..7a251052 100644 --- a/src/invidious/helpers/helpers.cr +++ b/src/invidious/helpers/helpers.cr @@ -313,13 +313,149 @@ def html_to_content(description_html : String) return description end -def extract_videos(nodeset, ucid = nil, author_name = nil) - videos = extract_items(nodeset, ucid, author_name) - videos.select { |item| item.is_a?(SearchVideo) }.map { |video| video.as(SearchVideo) } +def extract_videos(initial_data : Hash(String, JSON::Any)) + extract_items(initial_data).select(&.is_a?(SearchVideo)).map(&.as(SearchVideo)) end -def extract_items(nodeset, ucid = nil, author_name = nil) - # TODO: Make this a 'common', so it makes more sense to be used here +def extract_items(initial_data : Hash(String, JSON::Any)) + items = [] of SearchItem + + initial_data.try { |t| + t["contents"]? || t["response"]? + }.try { |t| + t["twoColumnBrowseResultsRenderer"]?.try &.["tabs"].as_a[0]?.try &.["tabRenderer"]["content"] || + t["twoColumnSearchResultsRenderer"]?.try &.["primaryContents"] || + t["continuationContents"]? + }.try { |t| t["sectionListRenderer"]? || t["sectionListContinuation"]? } + .try &.["contents"] + .as_a.each { |c| + c.try &.["itemSectionRenderer"]["contents"].as_a + .try { |t| t[0]?.try &.["shelfRenderer"]?.try &.["content"]["expandedShelfContentsRenderer"]?.try &.["items"].as_a || t } + .each { |item| + if i = item["videoRenderer"]? + video_id = i["videoId"].as_s + title = i["title"].try { |t| t["simpleText"]?.try &.as_s || t["runs"]?.try &.as_a.map(&.["text"].as_s).join("") } || "" + + author_info = i["ownerText"]?.try &.["runs"].as_a[0]? + author = author_info.try &.["text"].as_s || "" + author_id = author_info.try &.["navigationEndpoint"]?.try &.["browseEndpoint"]["browseId"].as_s || "" + + published = i["publishedTimeText"]?.try &.["simpleText"]?.try { |t| decode_date(t.as_s) } || Time.local + view_count = i["viewCountText"]?.try &.["simpleText"]?.try &.as_s.gsub(/\D+/, "").to_i64? || 0_i64 + description_html = i["descriptionSnippet"]?.try { |t| parse_content(t) } || "" + length_seconds = i["lengthText"]?.try &.["simpleText"]?.try &.as_s.try { |t| decode_length_seconds(t) } || 0 + + live_now = false + paid = false + premium = false + + premiere_timestamp = i["upcomingEventData"]?.try &.["startTime"]?.try { |t| Time.unix(t.as_s.to_i64) } + + i["badges"]?.try &.as_a.each do |badge| + b = badge["metadataBadgeRenderer"] + case b["label"].as_s + when "LIVE NOW" + live_now = true + when "New", "4K", "CC" + # TODO + when "Premium" + paid = true + + # TODO: Potentially available as i["topStandaloneBadge"]["metadataBadgeRenderer"] + premium = true + else nil # Ignore + end + end + + items << SearchVideo.new( + title: title, + id: video_id, + author: author, + ucid: author_id, + published: published, + views: view_count, + description_html: description_html, + length_seconds: length_seconds, + live_now: live_now, + paid: paid, + premium: premium, + premiere_timestamp: premiere_timestamp + ) + elsif i = item["channelRenderer"]? + author = i["title"]["simpleText"]?.try &.as_s || "" + author_id = i["channelId"]?.try &.as_s || "" + + author_thumbnail = i["thumbnail"]["thumbnails"]?.try &.as_a[0]?.try { |u| "https:#{u["url"]}" } || "" + subscriber_count = i["subscriberCountText"]?.try &.["simpleText"]?.try &.as_s.try { |s| short_text_to_number(s.split(" ")[0]) } || 0 + + auto_generated = false + auto_generated = true if !i["videoCountText"]? + video_count = i["videoCountText"]?.try &.["runs"].as_a[0]?.try &.["text"].as_s.gsub(/\D/, "").to_i || 0 + description_html = i["descriptionSnippet"]?.try { |t| parse_content(t) } || "" + + items << SearchChannel.new( + author: author, + ucid: author_id, + author_thumbnail: author_thumbnail, + subscriber_count: subscriber_count, + video_count: video_count, + description_html: description_html, + auto_generated: auto_generated, + ) + elsif i = item["playlistRenderer"]? + title = i["title"]["simpleText"]?.try &.as_s || "" + plid = i["playlistId"]?.try &.as_s || "" + + video_count = i["videoCount"]?.try &.as_s.to_i || 0 + playlist_thumbnail = i["thumbnails"].as_a[0]?.try &.["thumbnails"]?.try &.as_a[0]?.try &.["url"].as_s || "" + + author_info = i["shortBylineText"]["runs"].as_a[0]? + author = author_info.try &.["text"].as_s || "" + author_id = author_info.try &.["navigationEndpoint"]?.try &.["browseEndpoint"]["browseId"].as_s || "" + + videos = i["videos"]?.try &.as_a.map do |v| + v = v["childVideoRenderer"] + v_title = v["title"]["simpleText"]?.try &.as_s || "" + v_id = v["videoId"]?.try &.as_s || "" + v_length_seconds = v["lengthText"]?.try &.["simpleText"]?.try { |t| decode_length_seconds(t.as_s) } || 0 + SearchPlaylistVideo.new( + title: v_title, + id: v_id, + length_seconds: v_length_seconds + ) + end || [] of SearchPlaylistVideo + + # TODO: i["publishedTimeText"]? + + items << SearchPlaylist.new( + title: title, + id: plid, + author: author, + ucid: author_id, + video_count: video_count, + videos: videos, + thumbnail: playlist_thumbnail + ) + elsif i = item["radioRenderer"]? # Mix + # TODO + elsif i = item["showRenderer"]? # Show + # TODO + elsif i = item["shelfRenderer"]? + elsif i = item["horizontalCardListRenderer"]? + elsif i = item["searchPyvRenderer"]? # Ad + end + } + } + + items +end + +def extract_videos_html(nodeset, ucid = nil, author_name = nil) + extract_items_html(nodeset, ucid, author_name).select(&.is_a?(SearchVideo)).map(&.as(SearchVideo)) +end + +def extract_items_html(nodeset, ucid = nil, author_name = nil) + # TODO: Make this a 'CommonItem', so it makes more sense to be used here items = [] of SearchItem nodeset.each do |node| @@ -456,7 +592,7 @@ def extract_items(nodeset, ucid = nil, author_name = nil) paid = true end - premiere_timestamp = node.xpath_node(%q(.//ul[@class="yt-lockup-meta-info"]/li/span[@class="localized-date"])).try &.["data-timestamp"]?.try &.to_i64 + premiere_timestamp = node.xpath_node(%q(.//ul[@class="yt-lockup-meta-info"]/li/span[@class="localized-date"])).try &.["data-timestamp"]?.try &.to_i64? if premiere_timestamp premiere_timestamp = Time.unix(premiere_timestamp) end @@ -683,12 +819,12 @@ def check_table(db, logger, table_name, struct_type = nil) return if column_array.size <= struct_array.size - # column_array.each do |column| - # if !struct_array.includes? column - # logger.puts("ALTER TABLE #{table_name} DROP COLUMN #{column} CASCADE") - # db.exec("ALTER TABLE #{table_name} DROP COLUMN #{column} CASCADE") - # end - # end + column_array.each do |column| + if !struct_array.includes? column + logger.puts("ALTER TABLE #{table_name} DROP COLUMN #{column} CASCADE") + db.exec("ALTER TABLE #{table_name} DROP COLUMN #{column} CASCADE") + end + end end class PG::ResultSet @@ -864,12 +1000,12 @@ def create_notification_stream(env, topics, connection_channel) end end -def extract_initial_data(body) - initial_data = body.match(/window\["ytInitialData"\] = (?.*?);\n/).try &.["info"] || "{}" +def extract_initial_data(body) : Hash(String, JSON::Any) + initial_data = body.match(/window\["ytInitialData"\]\s*=\s*(?.*?);+\n/).try &.["info"] || "{}" if initial_data.starts_with?("JSON.parse(\"") - return JSON.parse(JSON.parse(%({"initial_data":"#{initial_data[12..-3]}"}))["initial_data"].as_s) + return JSON.parse(JSON.parse(%({"initial_data":"#{initial_data[12..-3]}"}))["initial_data"].as_s).as_h else - return JSON.parse(initial_data) + return JSON.parse(initial_data).as_h end end diff --git a/src/invidious/helpers/jobs.cr b/src/invidious/helpers/jobs.cr index a9aee064..e3d7b520 100644 --- a/src/invidious/helpers/jobs.cr +++ b/src/invidious/helpers/jobs.cr @@ -201,7 +201,7 @@ end def bypass_captcha(captcha_key, logger) loop do begin - {"/watch?v=CvFH_6DNRCY&gl=US&hl=en&disable_polymer=1&has_verified=1&bpctr=9999999999", produce_channel_videos_url(ucid: "UCXuqSBlHAE6Xw-yeJA0Tunw")}.each do |path| + {"/watch?v=CvFH_6DNRCY&gl=US&hl=en&has_verified=1&bpctr=9999999999", produce_channel_videos_url(ucid: "UCXuqSBlHAE6Xw-yeJA0Tunw")}.each do |path| response = YT_POOL.client &.get(path) if response.body.includes?("To continue with your YouTube experience, please fill out the form below.") html = XML.parse_html(response.body) diff --git a/src/invidious/helpers/signatures.cr b/src/invidious/helpers/signatures.cr index 0aaacd04..5eabb91b 100644 --- a/src/invidious/helpers/signatures.cr +++ b/src/invidious/helpers/signatures.cr @@ -1,8 +1,8 @@ alias SigProc = Proc(Array(String), Int32, Array(String)) def fetch_decrypt_function(id = "CvFH_6DNRCY") - document = YT_POOL.client &.get("/watch?v=#{id}&gl=US&hl=en&disable_polymer=1").body - url = document.match(/src="(?.*player_ias[^\/]+\/en_US\/base.js)"/).not_nil!["url"] + document = YT_POOL.client &.get("/watch?v=#{id}&gl=US&hl=en").body + url = document.match(/src="(?\/yts\/jsbin\/player_ias-[^\/]+\/en_US\/base.js)"/).not_nil!["url"] player = YT_POOL.client &.get(url).body function_name = player.match(/^(?[^=]+)=function\(\w\){\w=\w\.split\(""\);[^\. ]+\.[^( ]+/m).not_nil!["name"] diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index a39a0b16..a51f15ce 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -8,7 +8,7 @@ def add_yt_headers(request) request.headers["accept-language"] ||= "en-us,en;q=0.5" return if request.resource.starts_with? "/sorry/index" request.headers["x-youtube-client-name"] ||= "1" - request.headers["x-youtube-client-version"] ||= "1.20180719" + request.headers["x-youtube-client-version"] ||= "2.20200609" if !CONFIG.cookies.empty? request.headers["cookie"] = "#{(CONFIG.cookies.map { |c| "#{c.name}=#{c.value}" }).join("; ")}; #{request.headers["cookie"]?}" end diff --git a/src/invidious/mixes.cr b/src/invidious/mixes.cr index 04a37b87..6c01d78b 100644 --- a/src/invidious/mixes.cr +++ b/src/invidious/mixes.cr @@ -20,7 +20,6 @@ end def fetch_mix(rdid, video_id, cookies = nil, locale = nil) headers = HTTP::Headers.new - headers["User-Agent"] = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36" if cookies headers = cookies.add_request_headers(headers) diff --git a/src/invidious/search.cr b/src/invidious/search.cr index 7a88f316..b4bd6226 100644 --- a/src/invidious/search.cr +++ b/src/invidious/search.cr @@ -96,6 +96,10 @@ struct SearchVideo end end + def is_upcoming + premiere_timestamp ? true : false + end + db_mapping({ title: String, id: String, @@ -227,61 +231,35 @@ end alias SearchItem = SearchVideo | SearchChannel | SearchPlaylist def channel_search(query, page, channel) - response = YT_POOL.client &.get("/channel/#{channel}?disable_polymer=1&hl=en&gl=US") - document = XML.parse_html(response.body) - canonical = document.xpath_node(%q(//link[@rel="canonical"])) + response = YT_POOL.client &.get("/channel/#{channel}?hl=en&gl=US") + response = YT_POOL.client &.get("/user/#{channel}?hl=en&gl=US") if response.headers["location"]? + response = YT_POOL.client &.get("/c/#{channel}?hl=en&gl=US") if response.headers["location"]? - if !canonical - response = YT_POOL.client &.get("/c/#{channel}?disable_polymer=1&hl=en&gl=US") - document = XML.parse_html(response.body) - canonical = document.xpath_node(%q(//link[@rel="canonical"])) - end + ucid = response.body.match(/\\"channelId\\":\\"(?[^\\]+)\\"/).try &.["ucid"]? - if !canonical - response = YT_POOL.client &.get("/user/#{channel}?disable_polymer=1&hl=en&gl=US") - document = XML.parse_html(response.body) - canonical = document.xpath_node(%q(//link[@rel="canonical"])) - end - - if !canonical - return 0, [] of SearchItem - end - - ucid = canonical["href"].split("/")[-1] + return 0, [] of SearchItem if !ucid url = produce_channel_search_url(ucid, query, page) response = YT_POOL.client &.get(url) - json = JSON.parse(response.body) + initial_data = JSON.parse(response.body).as_a.find &.["response"]? + return 0, [] of SearchItem if !initial_data + items = extract_items(initial_data.as_h) - if json["content_html"]? && !json["content_html"].as_s.empty? - document = XML.parse_html(json["content_html"].as_s) - nodeset = document.xpath_nodes(%q(//li[contains(@class, "feed-item-container")])) - - count = nodeset.size - items = extract_items(nodeset) - else - count = 0 - items = [] of SearchItem - end - - return count, items + return items.size, items end def search(query, page = 1, search_params = produce_search_params(content_type: "all"), region = nil) - if query.empty? - return {0, [] of SearchItem} - end + return 0, [] of SearchItem if query.empty? - html = YT_POOL.client(region, &.get("/results?q=#{URI.encode_www_form(query)}&page=#{page}&sp=#{search_params}&hl=en&disable_polymer=1").body) - if html.empty? - return {0, [] of SearchItem} - end + body = YT_POOL.client(region, &.get("/results?q=#{URI.encode_www_form(query)}&page=#{page}&sp=#{search_params}&hl=en").body) + return 0, [] of SearchItem if body.empty? - html = XML.parse_html(html) - nodeset = html.xpath_nodes(%q(//ol[@class="item-section"]/li)) - items = extract_items(nodeset) + initial_data = extract_initial_data(body) + items = extract_items(initial_data) - return {nodeset.size, items} + # initial_data["estimatedResults"]?.try &.as_s.to_i64 + + return items.size, items end def produce_search_params(sort : String = "relevance", date : String = "", content_type : String = "", @@ -387,12 +365,9 @@ def produce_channel_search_url(ucid, query, page) "2:string" => ucid, "3:base64" => { "2:string" => "search", - "6:varint" => 2_i64, "7:varint" => 1_i64, - "12:varint" => 1_i64, - "13:string" => "", - "23:varint" => 0_i64, "15:string" => "#{page}", + "23:varint" => 0_i64, }, "11:string" => query, }, diff --git a/src/invidious/trending.cr b/src/invidious/trending.cr index 017c42f5..8d078387 100644 --- a/src/invidious/trending.cr +++ b/src/invidious/trending.cr @@ -1,7 +1,4 @@ def fetch_trending(trending_type, region, locale) - headers = HTTP::Headers.new - headers["User-Agent"] = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36" - region ||= "US" region = region.upcase @@ -11,7 +8,7 @@ def fetch_trending(trending_type, region, locale) if trending_type && trending_type != "Default" trending_type = trending_type.downcase.capitalize - response = YT_POOL.client &.get("/feed/trending?gl=#{region}&hl=en", headers).body + response = YT_POOL.client &.get("/feed/trending?gl=#{region}&hl=en").body initial_data = extract_initial_data(response) @@ -21,31 +18,28 @@ def fetch_trending(trending_type, region, locale) if url url["channelListSubMenuAvatarRenderer"]["navigationEndpoint"]["commandMetadata"]["webCommandMetadata"]["url"] url = url["channelListSubMenuAvatarRenderer"]["navigationEndpoint"]["commandMetadata"]["webCommandMetadata"]["url"].as_s - url += "&disable_polymer=1&gl=#{region}&hl=en" + url = "#{url}&gl=#{region}&hl=en" trending = YT_POOL.client &.get(url).body plid = extract_plid(url) else - trending = YT_POOL.client &.get("/feed/trending?gl=#{region}&hl=en&disable_polymer=1").body + trending = YT_POOL.client &.get("/feed/trending?gl=#{region}&hl=en").body end else - trending = YT_POOL.client &.get("/feed/trending?gl=#{region}&hl=en&disable_polymer=1").body + trending = YT_POOL.client &.get("/feed/trending?gl=#{region}&hl=en").body end - trending = XML.parse_html(trending) - nodeset = trending.xpath_nodes(%q(//ul/li[@class="expanded-shelf-content-item-wrapper"])) - trending = extract_videos(nodeset) + initial_data = extract_initial_data(trending) + trending = extract_videos(initial_data) return {trending, plid} end def extract_plid(url) - plid = URI.parse(url) - .try { |i| HTTP::Params.parse(i.query.not_nil!)["bp"] } + return url.try { |i| URI.parse(i).query } + .try { |i| HTTP::Params.parse(i)["bp"] } .try { |i| URI.decode_www_form(i) } .try { |i| Base64.decode(i) } .try { |i| IO::Memory.new(i) } .try { |i| Protodec::Any.parse(i) } - .try { |i| i["44:0:embedded"]["2:1:string"].as_s } - - return plid + .try &.["44:0:embedded"]?.try &.["2:1:string"]?.try &.as_s end diff --git a/src/invidious/users.cr b/src/invidious/users.cr index 0aa94d82..ba15692c 100644 --- a/src/invidious/users.cr +++ b/src/invidious/users.cr @@ -267,7 +267,7 @@ def subscribe_ajax(channel_id, action, env_headers) end headers = cookies.add_request_headers(headers) - if match = html.body.match(/'XSRF_TOKEN': "(?[A-Za-z0-9\_\-\=]+)"/) + if match = html.body.match(/'XSRF_TOKEN': "(?[^"]+)"/) session_token = match["session_token"] headers["content-type"] = "application/x-www-form-urlencoded" @@ -300,7 +300,7 @@ end # end # headers = cookies.add_request_headers(headers) # -# if match = html.body.match(/'XSRF_TOKEN': "(?[A-Za-z0-9\_\-\=]+)"/) +# if match = html.body.match(/'XSRF_TOKEN': "(?[^"]+)"/) # session_token = match["session_token"] # # headers["content-type"] = "application/x-www-form-urlencoded" diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index ed5847e4..f2638f14 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -246,12 +246,9 @@ struct VideoPreferences end struct Video - property player_json : JSON::Any? - property recommended_json : JSON::Any? - - module HTTPParamConverter + module JSONConverter def self.from_rs(rs) - HTTP::Params.parse(rs.read(String)) + JSON.parse(rs.read(String)).as_h end end @@ -271,7 +268,7 @@ struct Video generate_storyboards(json, self.id, self.storyboards) end - json.field "description", html_to_content(self.description_html) + json.field "description", self.description json.field "descriptionHtml", self.description_html json.field "published", self.published.to_unix json.field "publishedText", translate(locale, "`x` ago", recode_date(self.published, locale)) @@ -310,13 +307,13 @@ struct Video json.field "lengthSeconds", self.length_seconds json.field "allowRatings", self.allow_ratings - json.field "rating", self.info["avg_rating"].to_f32 + json.field "rating", self.average_rating json.field "isListed", self.is_listed json.field "liveNow", self.live_now json.field "isUpcoming", self.is_upcoming if self.premiere_timestamp - json.field "premiereTimestamp", self.premiere_timestamp.not_nil!.to_unix + json.field "premiereTimestamp", self.premiere_timestamp.try &.to_unix end if hlsvp = self.hls_manifest_url @@ -328,21 +325,21 @@ struct Video json.field "adaptiveFormats" do json.array do - self.adaptive_fmts(decrypt_function).each do |fmt| + self.adaptive_fmts.each do |fmt| json.object do - json.field "index", fmt["index"] - json.field "bitrate", fmt["bitrate"] - json.field "init", fmt["init"] + json.field "index", "#{fmt["indexRange"]["start"]}-#{fmt["indexRange"]["end"]}" + json.field "bitrate", fmt["bitrate"].as_i.to_s + json.field "init", "#{fmt["initRange"]["start"]}-#{fmt["initRange"]["end"]}" json.field "url", fmt["url"] - json.field "itag", fmt["itag"] - json.field "type", fmt["type"] - json.field "clen", fmt["clen"] - json.field "lmt", fmt["lmt"] - json.field "projectionType", fmt["projection_type"] + json.field "itag", fmt["itag"].as_i.to_s + json.field "type", fmt["mimeType"] + json.field "clen", fmt["contentLength"] + json.field "lmt", fmt["lastModified"] + json.field "projectionType", fmt["projectionType"] fmt_info = itag_to_metadata?(fmt["itag"]) if fmt_info - fps = fmt_info["fps"]?.try &.to_i || fmt["fps"]?.try &.to_i || 30 + fps = fmt_info["fps"]?.try &.to_i || fmt["fps"]?.try &.as_i || 30 json.field "fps", fps json.field "container", fmt_info["ext"] json.field "encoding", fmt_info["vcodec"]? || fmt_info["acodec"] @@ -368,16 +365,16 @@ struct Video json.field "formatStreams" do json.array do - self.fmt_stream(decrypt_function).each do |fmt| + self.fmt_stream.each do |fmt| json.object do json.field "url", fmt["url"] - json.field "itag", fmt["itag"] - json.field "type", fmt["type"] + json.field "itag", fmt["itag"].as_i.to_s + json.field "type", fmt["mimeType"] json.field "quality", fmt["quality"] fmt_info = itag_to_metadata?(fmt["itag"]) if fmt_info - fps = fmt_info["fps"]?.try &.to_i || fmt["fps"]?.try &.to_i || 30 + fps = fmt_info["fps"]?.try &.to_i || fmt["fps"]?.try &.as_i || 30 json.field "fps", fps json.field "container", fmt_info["ext"] json.field "encoding", fmt_info["vcodec"]? || fmt_info["acodec"] @@ -415,9 +412,7 @@ struct Video json.field "recommendedVideos" do json.array do - self.info["rvs"]?.try &.split(",").each do |rv| - rv = HTTP::Params.parse(rv) - + self.related_videos.each do |rv| if rv["id"]? json.object do json.field "videoId", rv["id"] @@ -436,7 +431,7 @@ struct Video qualities.each do |quality| json.object do - json.field "url", rv["author_thumbnail"].gsub(/s\d+-/, "s#{quality}-") + json.field "url", rv["author_thumbnail"]?.try &.gsub(/s\d+-/, "s#{quality}-") json.field "width", quality json.field "height", quality end @@ -445,9 +440,9 @@ struct Video end end - json.field "lengthSeconds", rv["length_seconds"].to_i - json.field "viewCountText", rv["short_view_count_text"] - json.field "viewCount", rv["view_count"]?.try &.to_i64 + json.field "lengthSeconds", rv["length_seconds"]?.try &.to_i + json.field "viewCountText", rv["short_view_count_text"]? + json.field "viewCount", rv["view_count"]?.try &.empty? ? nil : rv["view_count"].to_i64 end end end @@ -466,256 +461,150 @@ struct Video end end - # `description_html` is stored in DB as `description`, which can be - # quite confusing. Since it currently isn't very practical to rename - # it, we instead define a getter and setter here. - def description_html - self.description + def title + info["videoDetails"]["title"]?.try &.as_s || "" end - def description_html=(other : String) - self.description = other + def ucid + info["videoDetails"]["channelId"]?.try &.as_s || "" + end + + def author + info["videoDetails"]["author"]?.try &.as_s || "" + end + + def length_seconds : Int32 + info["microformat"]?.try &.["playerMicroformatRenderer"]?.try &.["lengthSeconds"]?.try &.as_s.to_i || + info["videoDetails"]["lengthSeconds"]?.try &.as_s.to_i || 0 + end + + def views : Int64 + info["videoDetails"]["viewCount"]?.try &.as_s.to_i64 || 0_i64 + end + + def likes : Int64 + info["likes"]?.try &.as_i64 || 0_i64 + end + + def dislikes : Int64 + info["dislikes"]?.try &.as_i64 || 0_i64 + end + + def average_rating : Float64 + # (likes / (likes + dislikes) * 4 + 1) + info["videoDetails"]["averageRating"]?.try { |t| t.as_f? || t.as_i64?.try &.to_f64 }.try &.round(4) || 0.0 + end + + def published : Time + info["microformat"]?.try &.["playerMicroformatRenderer"]?.try &.["publishDate"]?.try { |t| Time.parse(t.as_s, "%Y-%m-%d", Time::Location.local) } || Time.local + end + + def published=(other : Time) + info["microformat"].as_h["playerMicroformatRenderer"].as_h["publishDate"] = JSON::Any.new(other.to_s("%Y-%m-%d")) + end + + def cookie + info["cookie"]?.try &.as_h.map { |k, v| "#{k}=#{v}" }.join("; ") || "" end def allow_ratings - allow_ratings = player_response["videoDetails"]?.try &.["allowRatings"]?.try &.as_bool - - if allow_ratings.nil? - return true - end - - return allow_ratings + r = info["videoDetails"]["allowRatings"]?.try &.as_bool + r.nil? ? false : r end def live_now - live_now = player_response["videoDetails"]?.try &.["isLive"]?.try &.as_bool - - if live_now.nil? - return false - end - - return live_now + info["videoDetails"]["isLiveContent"]?.try &.as_bool || false end def is_listed - is_listed = player_response["videoDetails"]?.try &.["isCrawlable"]?.try &.as_bool - - if is_listed.nil? - return true - end - - return is_listed + info["videoDetails"]["isCrawlable"]?.try &.as_bool || false end def is_upcoming - is_upcoming = player_response["videoDetails"]?.try &.["isUpcoming"]?.try &.as_bool - - if is_upcoming.nil? - return false - end - - return is_upcoming + info["videoDetails"]["isUpcoming"]?.try &.as_bool || false end - def premiere_timestamp - if self.is_upcoming - premiere_timestamp = player_response["playabilityStatus"]? - .try &.["liveStreamability"]? - .try &.["liveStreamabilityRenderer"]? - .try &.["offlineSlate"]? - .try &.["liveStreamOfflineSlateRenderer"]? - .try &.["scheduledStartTime"]?.try &.as_s.to_i64 - end - - if premiere_timestamp - premiere_timestamp = Time.unix(premiere_timestamp) - end - - return premiere_timestamp + def premiere_timestamp : Time? + info["microformat"]?.try &.["playerMicroformatRenderer"]? + .try &.["liveBroadcastDetails"]?.try &.["startTimestamp"]?.try { |t| Time.parse_rfc3339(t.as_s) } end def keywords - keywords = player_response["videoDetails"]?.try &.["keywords"]?.try &.as_a - keywords ||= [] of String - - return keywords + info["videoDetails"]["keywords"]?.try &.as_a.map &.as_s || [] of String end - def fmt_stream(decrypt_function) - streams = [] of HTTP::Params - - if fmt_streams = player_response["streamingData"]?.try &.["formats"]? - fmt_streams.as_a.each do |fmt_stream| - if !fmt_stream.as_h? - next - end - - fmt = {} of String => String - - fmt["lmt"] = fmt_stream["lastModified"]?.try &.as_s || "0" - fmt["projection_type"] = "1" - fmt["type"] = fmt_stream["mimeType"].as_s - fmt["clen"] = fmt_stream["contentLength"]?.try &.as_s || "0" - fmt["bitrate"] = fmt_stream["bitrate"]?.try &.as_i.to_s || "0" - fmt["itag"] = fmt_stream["itag"].as_i.to_s - if fmt_stream["url"]? - fmt["url"] = fmt_stream["url"].as_s - end - if cipher = fmt_stream["cipher"]? || fmt_stream["signatureCipher"]? - HTTP::Params.parse(cipher.as_s).each do |key, value| - fmt[key] = value - end - end - fmt["quality"] = fmt_stream["quality"].as_s - - if fmt_stream["width"]? - fmt["size"] = "#{fmt_stream["width"]}x#{fmt_stream["height"]}" - fmt["height"] = fmt_stream["height"].as_i.to_s - end - - if fmt_stream["fps"]? - fmt["fps"] = fmt_stream["fps"].as_i.to_s - end - - if fmt_stream["qualityLabel"]? - fmt["quality_label"] = fmt_stream["qualityLabel"].as_s - end - - params = HTTP::Params.new - fmt.each do |key, value| - params[key] = value - end - - streams << params - end - - streams.sort_by! { |stream| stream["height"].to_i }.reverse! - elsif fmt_stream = self.info["url_encoded_fmt_stream_map"]? - fmt_stream.split(",").each do |string| - if !string.empty? - streams << HTTP::Params.parse(string) - end - end - end - - streams.each { |s| s.add("label", "#{s["quality"]} - #{s["type"].split(";")[0].split("/")[1]}") } - streams = streams.uniq { |s| s["label"] } - - if self.info["region"]? - streams.each do |fmt| - fmt["url"] += "®ion=" + self.info["region"] - end - end - - streams.each do |fmt| - fmt["url"] += "&host=" + (URI.parse(fmt["url"]).host || "") - fmt["url"] += decrypt_signature(fmt, decrypt_function) - end - - return streams + def related_videos + info["relatedVideos"]?.try &.as_a.map { |h| h.as_h.transform_values &.as_s } || [] of Hash(String, String) end - def adaptive_fmts(decrypt_function) - adaptive_fmts = [] of HTTP::Params - - if fmts = player_response["streamingData"]?.try &.["adaptiveFormats"]? - fmts.as_a.each do |adaptive_fmt| - next if !adaptive_fmt.as_h? - fmt = {} of String => String - - if init = adaptive_fmt["initRange"]? - fmt["init"] = "#{init["start"]}-#{init["end"]}" - end - fmt["init"] ||= "0-0" - - fmt["lmt"] = adaptive_fmt["lastModified"]?.try &.as_s || "0" - fmt["projection_type"] = "1" - fmt["type"] = adaptive_fmt["mimeType"].as_s - fmt["clen"] = adaptive_fmt["contentLength"]?.try &.as_s || "0" - fmt["bitrate"] = adaptive_fmt["bitrate"]?.try &.as_i.to_s || "0" - fmt["itag"] = adaptive_fmt["itag"].as_i.to_s - if adaptive_fmt["url"]? - fmt["url"] = adaptive_fmt["url"].as_s - end - if cipher = adaptive_fmt["cipher"]? || adaptive_fmt["signatureCipher"]? - HTTP::Params.parse(cipher.as_s).each do |key, value| - fmt[key] = value - end - end - if index = adaptive_fmt["indexRange"]? - fmt["index"] = "#{index["start"]}-#{index["end"]}" - end - fmt["index"] ||= "0-0" - - if adaptive_fmt["width"]? - fmt["size"] = "#{adaptive_fmt["width"]}x#{adaptive_fmt["height"]}" - end - - if adaptive_fmt["fps"]? - fmt["fps"] = adaptive_fmt["fps"].as_i.to_s - end - - if adaptive_fmt["qualityLabel"]? - fmt["quality_label"] = adaptive_fmt["qualityLabel"].as_s - end - - params = HTTP::Params.new - fmt.each do |key, value| - params[key] = value - end - - adaptive_fmts << params - end - elsif fmts = self.info["adaptive_fmts"]? - fmts.split(",") do |string| - adaptive_fmts << HTTP::Params.parse(string) - end - end - - if self.info["region"]? - adaptive_fmts.each do |fmt| - fmt["url"] += "®ion=" + self.info["region"] - end - end - - adaptive_fmts.each do |fmt| - fmt["url"] += "&host=" + (URI.parse(fmt["url"]).host || "") - fmt["url"] += decrypt_signature(fmt, decrypt_function) - end - - return adaptive_fmts + def allowed_regions + info["microformat"]?.try &.["playerMicroformatRenderer"]? + .try &.["availableCountries"]?.try &.as_a.map &.as_s || [] of String end - def video_streams(adaptive_fmts) - video_streams = adaptive_fmts.select { |s| s["type"].starts_with? "video" } - - return video_streams + def author_thumbnail : String + info["authorThumbnail"]?.try &.as_s || "" end - def audio_streams(adaptive_fmts) - audio_streams = adaptive_fmts.select { |s| s["type"].starts_with? "audio" } - audio_streams.sort_by! { |s| s["bitrate"].to_i }.reverse! - audio_streams.each do |stream| - stream["bitrate"] = (stream["bitrate"].to_f64/1000).to_i.to_s + def sub_count_text : String + info["subCountText"]?.try &.as_s || "-" + end + + def fmt_stream + return @fmt_stream.as(Array(Hash(String, JSON::Any))) if @fmt_stream + fmt_stream = info["streamingData"]?.try &.["formats"]?.try &.as_a.map &.as_h || [] of Hash(String, JSON::Any) + fmt_stream.each do |fmt| + if s = (fmt["cipher"]? || fmt["signatureCipher"]?).try { |h| HTTP::Params.parse(h.as_s) } + s.each do |k, v| + fmt[k] = JSON::Any.new(v) + end + fmt["url"] = JSON::Any.new("#{fmt["url"]}#{decrypt_signature(fmt)}") + end + + fmt["url"] = JSON::Any.new("#{fmt["url"]}&host=#{URI.parse(fmt["url"].as_s).host}") + fmt["url"] = JSON::Any.new("#{fmt["url"]}®ion=#{self.info["region"]}") if self.info["region"]? end - - return audio_streams + fmt_stream.sort_by! { |f| f["width"]?.try &.as_i || 0 } + @fmt_stream = fmt_stream + return @fmt_stream.as(Array(Hash(String, JSON::Any))) end - def player_response - @player_json = JSON.parse(@info["player_response"]) if !@player_json - @player_json.not_nil! + def adaptive_fmts + return @adaptive_fmts.as(Array(Hash(String, JSON::Any))) if @adaptive_fmts + fmt_stream = info["streamingData"]?.try &.["adaptiveFormats"]?.try &.as_a.map &.as_h || [] of Hash(String, JSON::Any) + fmt_stream.each do |fmt| + if s = (fmt["cipher"]? || fmt["signatureCipher"]?).try { |h| HTTP::Params.parse(h.as_s) } + s.each do |k, v| + fmt[k] = JSON::Any.new(v) + end + fmt["url"] = JSON::Any.new("#{fmt["url"]}#{decrypt_signature(fmt)}") + end + + fmt["url"] = JSON::Any.new("#{fmt["url"]}&host=#{URI.parse(fmt["url"].as_s).host}") + fmt["url"] = JSON::Any.new("#{fmt["url"]}®ion=#{self.info["region"]}") if self.info["region"]? + end + fmt_stream.sort_by! { |f| f["width"]?.try &.as_i || 0 } + @adaptive_fmts = fmt_stream + return @adaptive_fmts.as(Array(Hash(String, JSON::Any))) + end + + def video_streams + adaptive_fmts.select &.["mimeType"]?.try &.as_s.starts_with?("video") + end + + def audio_streams + adaptive_fmts.select &.["mimeType"]?.try &.as_s.starts_with?("audio") end def storyboards - storyboards = player_response["storyboards"]? + storyboards = info["storyboards"]? .try &.as_h .try &.["playerStoryboardSpecRenderer"]? .try &.["spec"]? .try &.as_s.split("|") if !storyboards - if storyboard = player_response["storyboards"]? + if storyboard = info["storyboards"]? .try &.as_h .try &.["playerLiveStoryboardSpecRenderer"]? .try &.["spec"]? @@ -743,9 +632,7 @@ struct Video storyboard_height: Int32, storyboard_count: Int32) - if !storyboards - return items - end + return items if !storyboards url = URI.parse(storyboards.shift) params = HTTP::Params.parse(url.query || "") @@ -779,82 +666,98 @@ struct Video end def paid - reason = player_response["playabilityStatus"]?.try &.["reason"]? + reason = info["playabilityStatus"]?.try &.["reason"]? paid = reason == "This video requires payment to watch." ? true : false - - return paid + paid end def premium - if info["premium"]? - self.info["premium"] == "true" - else - false - end + keywords.includes? "YouTube Red" end - def captions - captions = [] of Caption - if player_response["captions"]? - caption_list = player_response["captions"]["playerCaptionsTracklistRenderer"]["captionTracks"]?.try &.as_a - caption_list ||= [] of JSON::Any - - caption_list.each do |caption| - caption = Caption.from_json(caption.to_json) - caption.name.simpleText = caption.name.simpleText.split(" - ")[0] - captions << caption - end + def captions : Array(Caption) + return @captions.as(Array(Caption)) if @captions + captions = info["captions"]?.try &.["playerCaptionsTracklistRenderer"]?.try &.["captionTracks"]?.try &.as_a.map do |caption| + caption = Caption.from_json(caption.to_json) + caption.name.simpleText = caption.name.simpleText.split(" - ")[0] + caption end + captions ||= [] of Caption + @captions = captions + return @captions.as(Array(Caption)) + end - return captions + def description + description = info["microformat"]?.try &.["playerMicroformatRenderer"]? + .try &.["description"]?.try &.["simpleText"]?.try &.as_s || "" + end + + # TODO + def description=(value : String) + @description = value + end + + def description_html + info["descriptionHtml"]?.try &.as_s || "

" + end + + def description_html=(value : String) + info["descriptionHtml"] = JSON::Any.new(value) end def short_description - short_description = self.description_html.gsub(/(
)|(|"|\n)/, { - "
": " ", - "
": " ", - "\"": """, - "\n": " ", - }) - short_description = XML.parse_html(short_description).content[0..200].strip(" ") - - if short_description.empty? - short_description = " " - end - - return short_description + info["shortDescription"]?.try &.as_s || "" end - def length_seconds - player_response["videoDetails"]["lengthSeconds"].as_s.to_i + def hls_manifest_url : String? + info["streamingData"]?.try &.["hlsManifestUrl"]?.try &.as_s + end + + def dash_manifest_url + info["streamingData"]?.try &.["dashManifestUrl"]?.try &.as_s + end + + def genre : String + info["genre"]?.try &.as_s || "" + end + + def genre_url : String + info["genreUcid"]? ? "/channel/#{info["genreUcid"]}" : "" + end + + def license : String? + info["license"]?.try &.as_s + end + + def is_family_friendly : Bool + info["microformat"]?.try &.["playerMicroformatRenderer"]["isFamilySafe"]?.try &.as_bool || false + end + + def wilson_score : Float64 + ci_lower_bound(likes, likes + dislikes).round(4) + end + + def engagement : Float64 + ((likes + dislikes) / views).round(4) + end + + def reason : String? + info["reason"]?.try &.as_s + end + + def session_token : String? + info["sessionToken"]?.try &.as_s? end db_mapping({ - id: String, - info: { - type: HTTP::Params, - default: HTTP::Params.parse(""), - converter: Video::HTTPParamConverter, - }, - updated: Time, - title: String, - views: Int64, - likes: Int32, - dislikes: Int32, - wilson_score: Float64, - published: Time, - description: String, - language: String?, - author: String, - ucid: String, - allowed_regions: Array(String), - is_family_friendly: Bool, - genre: String, - genre_url: String, - license: String, - sub_count_text: String, - author_thumbnail: String, + id: String, + info: {type: Hash(String, JSON::Any), converter: Video::JSONConverter}, + updated: Time, }) + + @captions : Array(Caption)? + @adaptive_fmts : Array(Hash(String, JSON::Any))? + @fmt_stream : Array(Hash(String, JSON::Any))? end struct Caption @@ -878,121 +781,64 @@ class VideoRedirect < Exception end end -def get_video(id, db, refresh = true, region = nil, force_refresh = false) - if (video = db.query_one?("SELECT * FROM videos WHERE id = $1", id, as: Video)) && !region - # If record was last updated over 10 minutes ago, or video has since premiered, - # refresh (expire param in response lasts for 6 hours) - if (refresh && - (Time.utc - video.updated > 10.minutes) || - (video.premiere_timestamp && video.premiere_timestamp.as(Time) < Time.utc)) || - force_refresh - begin - video = fetch_video(id, region) - video_array = video.to_a +def parse_related(r : JSON::Any) : JSON::Any? + # TODO: r["endScreenPlaylistRenderer"], etc. + return if !r["endScreenVideoRenderer"]? + r = r["endScreenVideoRenderer"].as_h - args = arg_array(video_array[1..-1], 2) + return if !r["lengthInSeconds"]? - db.exec("UPDATE videos SET (info,updated,title,views,likes,dislikes,wilson_score,\ - published,description,language,author,ucid,allowed_regions,is_family_friendly,\ - genre,genre_url,license,sub_count_text,author_thumbnail)\ - = (#{args}) WHERE id = $1", args: video_array) - rescue ex - db.exec("DELETE FROM videos * WHERE id = $1", id) - raise ex - end - end - else - video = fetch_video(id, region) - video_array = video.to_a - - args = arg_array(video_array) - - if !region - db.exec("INSERT INTO videos VALUES (#{args}) ON CONFLICT (id) DO NOTHING", args: video_array) - end - end - - return video + rv = {} of String => JSON::Any + rv["author"] = r["shortBylineText"]["runs"][0]?.try &.["text"] || JSON::Any.new("") + rv["ucid"] = r["shortBylineText"]["runs"][0]?.try &.["navigationEndpoint"]["browseEndpoint"]["browseId"] || JSON::Any.new("") + rv["author_url"] = JSON::Any.new("/channel/#{rv["ucid"]}") + rv["length_seconds"] = JSON::Any.new(r["lengthInSeconds"].as_i.to_s) + rv["title"] = r["title"]["simpleText"] + rv["short_view_count_text"] = JSON::Any.new(r["shortViewCountText"]?.try &.["simpleText"]?.try &.as_s || "") + rv["view_count"] = JSON::Any.new(r["title"]["accessibility"]?.try &.["accessibilityData"]["label"].as_s.match(/(?[1-9](\d+,?)*) views/).try &.["views"].gsub(/\D/, "") || "") + rv["id"] = r["videoId"] + JSON::Any.new(rv) end -def extract_recommended(recommended_videos) - rvs = [] of HTTP::Params +def extract_polymer_config(body) + params = {} of String => JSON::Any + player_response = body.match(/window\["ytInitialPlayerResponse"\]\s*=\s*(?.*?);\n/) + .try { |r| JSON.parse(r["info"]).as_h } - recommended_videos.try &.each do |compact_renderer| - if compact_renderer["compactRadioRenderer"]? || compact_renderer["compactPlaylistRenderer"]? - # TODO - elsif video_renderer = compact_renderer["compactVideoRenderer"]? - recommended_video = HTTP::Params.new - recommended_video["id"] = video_renderer["videoId"].as_s - recommended_video["title"] = video_renderer["title"]["simpleText"].as_s - - next if !video_renderer["shortBylineText"]? - - recommended_video["author"] = video_renderer["shortBylineText"]["runs"].as_a[0]["text"].as_s - recommended_video["ucid"] = video_renderer["shortBylineText"]["runs"].as_a[0]["navigationEndpoint"]["browseEndpoint"]["browseId"].as_s - recommended_video["author_thumbnail"] = video_renderer["channelThumbnail"]["thumbnails"][0]["url"].as_s - - if view_count = video_renderer["viewCountText"]?.try { |field| field["simpleText"]?.try &.as_s || field["runs"][0]?.try &.["text"].as_s }.try &.delete(", views watching").to_i64?.try &.to_s - recommended_video["view_count"] = view_count - recommended_video["short_view_count_text"] = "#{number_to_short_text(view_count.to_i64)} views" - end - recommended_video["length_seconds"] = decode_length_seconds(video_renderer["lengthText"]?.try &.["simpleText"]?.try &.as_s || "0:00").to_s - - rvs << recommended_video - end + if body.includes?("To continue with your YouTube experience, please fill out the form below.") || + body.includes?("https://www.google.com/sorry/index") + params["reason"] = JSON::Any.new("Could not extract video info. Instance is likely blocked.") + elsif !player_response + params["reason"] = JSON::Any.new("Video unavailable.") + elsif player_response["playabilityStatus"]?.try &.["status"]?.try &.as_s != "OK" + reason = player_response["playabilityStatus"]["errorScreen"]?.try &.["playerErrorMessageRenderer"]?.try &.["subreason"]?.try { |s| s["simpleText"]?.try &.as_s || s["runs"].as_a.map { |r| r["text"] }.join("") } || + player_response["playabilityStatus"]["reason"].as_s + params["reason"] = JSON::Any.new(reason) end - rvs -end + params["sessionToken"] = JSON::Any.new(body.match(/"XSRF_TOKEN":"(?[^"]+)"/).try &.["session_token"]?) + params["shortDescription"] = JSON::Any.new(body.match(/"og:description" content="(?[^"]+)"/).try &.["description"]?) -def extract_polymer_config(body, html) - params = HTTP::Params.new + return params if !player_response - params["session_token"] = body.match(/"XSRF_TOKEN":"(?[A-Za-z0-9\_\-\=]+)"/).try &.["session_token"] || "" - - html_info = JSON.parse(body.match(/ytplayer\.config = (?.*?);ytplayer\.load/).try &.["info"] || "{}").try &.["args"]?.try &.as_h - - if html_info - html_info.each do |key, value| - params[key] = value.to_s - end + {"captions", "microformat", "playabilityStatus", "storyboards", "videoDetails"}.each do |f| + params[f] = player_response[f] if player_response[f]? end - initial_data = extract_initial_data(body) + yt_initial_data = body.match(/window\["ytInitialData"\]\s*=\s*(?.*?);\n/) + .try { |r| JSON.parse(r["info"]).as_h } - primary_results = initial_data["contents"]? - .try &.["twoColumnWatchNextResults"]? - .try &.["results"]? - .try &.["results"]? - .try &.["contents"]? - - comment_continuation = primary_results.try &.as_a.select { |object| object["itemSectionRenderer"]? }[0]? - .try &.["itemSectionRenderer"]? - .try &.["continuations"]? - .try &.[0]? - .try &.["nextContinuationData"]? - - params["ctoken"] = comment_continuation.try &.["continuation"]?.try &.as_s || "" - params["itct"] = comment_continuation.try &.["clickTrackingParams"]?.try &.as_s || "" - - rvs = initial_data["contents"]? - .try &.["twoColumnWatchNextResults"]? - .try &.["secondaryResults"]? - .try &.["secondaryResults"]? - .try &.["results"]? - .try &.as_a - - params["rvs"] = extract_recommended(rvs).join(",") - - # TODO: Watching now - params["views"] = primary_results.try &.as_a.select { |object| object["videoPrimaryInfoRenderer"]? }[0]? - .try &.["videoPrimaryInfoRenderer"]? - .try &.["viewCount"]? - .try &.["videoViewCountRenderer"]? - .try &.["viewCount"]? - .try &.["simpleText"]? - .try &.as_s.gsub(/\D/, "").to_i64.to_s || "0" + params["relatedVideos"] = yt_initial_data.try &.["playerOverlays"]?.try &.["playerOverlayRenderer"]? + .try &.["endScreen"]?.try &.["watchNextEndScreenRenderer"]?.try &.["results"]?.try &.as_a.compact_map { |r| + parse_related r + }.try { |a| JSON::Any.new(a) } || yt_initial_data.try &.["webWatchNextResponseExtensionData"]?.try &.["relatedVideoArgs"]? + .try &.as_s.split(",").map { |r| + r = HTTP::Params.parse(r).to_h + JSON::Any.new(Hash.zip(r.keys, r.values.map { |v| JSON::Any.new(v) })) + }.try { |a| JSON::Any.new(a) } || JSON::Any.new([] of JSON::Any) + primary_results = yt_initial_data.try &.["contents"]?.try &.["twoColumnWatchNextResults"]?.try &.["results"]? + .try &.["results"]?.try &.["contents"]? sentiment_bar = primary_results.try &.as_a.select { |object| object["videoPrimaryInfoRenderer"]? }[0]? .try &.["videoPrimaryInfoRenderer"]? .try &.["sentimentBar"]? @@ -1000,34 +846,13 @@ def extract_polymer_config(body, html) .try &.["tooltip"]? .try &.as_s - likes, dislikes = sentiment_bar.try &.split(" / ").map { |a| a.delete(", ").to_i32 }[0, 2] || {0, 0} + likes, dislikes = sentiment_bar.try &.split(" / ", 2).map &.gsub(/\D/, "").to_i64 || {0_i64, 0_i64} + params["likes"] = JSON::Any.new(likes) + params["dislikes"] = JSON::Any.new(dislikes) - params["likes"] = "#{likes}" - params["dislikes"] = "#{dislikes}" - - published = primary_results.try &.as_a.select { |object| object["videoSecondaryInfoRenderer"]? }[0]? - .try &.["videoSecondaryInfoRenderer"]? - .try &.["dateText"]? - .try &.["simpleText"]? - .try &.as_s.split(" ")[-3..-1].join(" ") - - if published - params["published"] = Time.parse(published, "%b %-d, %Y", Time::Location.local).to_unix.to_s - else - params["published"] = Time.utc(1990, 1, 1).to_unix.to_s - end - - params["description_html"] = "

" - - description_html = primary_results.try &.as_a.select { |object| object["videoSecondaryInfoRenderer"]? }[0]? - .try &.["videoSecondaryInfoRenderer"]? - .try &.["description"]? - .try &.["runs"]? - .try &.as_a - - if description_html - params["description_html"] = content_to_comment_html(description_html) - end + params["descriptionHtml"] = JSON::Any.new(primary_results.try &.as_a.select { |object| object["videoSecondaryInfoRenderer"]? }[0]? + .try &.["videoSecondaryInfoRenderer"]?.try &.["description"]?.try &.["runs"]? + .try &.as_a.try { |t| content_to_comment_html(t).gsub("\n", "
") } || "

") metadata = primary_results.try &.as_a.select { |object| object["videoSecondaryInfoRenderer"]? }[0]? .try &.["videoSecondaryInfoRenderer"]? @@ -1036,10 +861,6 @@ def extract_polymer_config(body, html) .try &.["rows"]? .try &.as_a - params["genre"] = "" - params["genre_ucid"] = "" - params["license"] = "" - metadata.try &.each do |row| title = row["metadataRowRenderer"]?.try &.["title"]?.try &.["simpleText"]?.try &.as_s contents = row["metadataRowRenderer"]? @@ -1050,220 +871,125 @@ def extract_polymer_config(body, html) contents = contents.try &.["runs"]? .try &.as_a[0]? - params["genre"] = contents.try &.["text"]? - .try &.as_s || "" - params["genre_ucid"] = contents.try &.["navigationEndpoint"]? - .try &.["browseEndpoint"]? - .try &.["browseId"]?.try &.as_s || "" + params["genre"] = JSON::Any.new(contents.try &.["text"]?.try &.as_s || "") + params["genreUcid"] = JSON::Any.new(contents.try &.["navigationEndpoint"]?.try &.["browseEndpoint"]? + .try &.["browseId"]?.try &.as_s || "") elsif title.try &.== "License" contents = contents.try &.["runs"]? .try &.as_a[0]? - params["license"] = contents.try &.["text"]? - .try &.as_s || "" + params["license"] = JSON::Any.new(contents.try &.["text"]?.try &.as_s || "") elsif title.try &.== "Licensed to YouTube by" - params["license"] = contents.try &.["simpleText"]? - .try &.as_s || "" + params["license"] = JSON::Any.new(contents.try &.["simpleText"]?.try &.as_s || "") end end author_info = primary_results.try &.as_a.select { |object| object["videoSecondaryInfoRenderer"]? }[0]? - .try &.["videoSecondaryInfoRenderer"]? - .try &.["owner"]? - .try &.["videoOwnerRenderer"]? + .try &.["videoSecondaryInfoRenderer"]?.try &.["owner"]?.try &.["videoOwnerRenderer"]? - params["author_thumbnail"] = author_info.try &.["thumbnail"]? - .try &.["thumbnails"]? - .try &.as_a[0]? - .try &.["url"]? - .try &.as_s || "" + params["authorThumbnail"] = JSON::Any.new(author_info.try &.["thumbnail"]? + .try &.["thumbnails"]?.try &.as_a[0]?.try &.["url"]? + .try &.as_s || "") - params["sub_count_text"] = author_info.try &.["subscriberCountText"]? - .try &.["simpleText"]? - .try &.as_s.gsub(/\D/, "") || "0" + params["subCountText"] = JSON::Any.new(author_info.try &.["subscriberCountText"]? + .try { |t| t["simpleText"]? || t["runs"]?.try &.[0]?.try &.["text"]? }.try &.as_s.split(" ", 2)[0] || "-") - return params + initial_data = body.match(/ytplayer\.config\s*=\s*(?.*?);ytplayer\.web_player_context_config/) + .try { |r| JSON.parse(r["info"]) }.try &.["args"]["player_response"]? + .try &.as_s?.try &.try { |r| JSON.parse(r).as_h } + + return params if !initial_data + + {"playabilityStatus", "streamingData"}.each do |f| + params[f] = initial_data[f] if initial_data[f]? + end + + params end -def extract_player_config(body, html) - params = HTTP::Params.new - - if md = body.match(/'XSRF_TOKEN': "(?[A-Za-z0-9\_\-\=]+)"/) - params["session_token"] = md["session_token"] - end - - if md = body.match(/'RELATED_PLAYER_ARGS': (?.*?),\n/) - recommended_json = JSON.parse(md["json"]) - rvs_params = recommended_json["rvs"].as_s.split(",").map { |params| HTTP::Params.parse(params) } - - if watch_next_response = recommended_json["watch_next_response"]? - watch_next_json = JSON.parse(watch_next_response.as_s) - rvs = watch_next_json["contents"]? - .try &.["twoColumnWatchNextResults"]? - .try &.["secondaryResults"]? - .try &.["secondaryResults"]? - .try &.["results"]? - .try &.as_a - - rvs = extract_recommended(rvs).compact_map do |rv| - if !rv["short_view_count_text"]? - rv_params = rvs_params.select { |rv_params| rv_params["id"]? == (rv["id"]? || "") }[0]? - - if rv_params.try &.["short_view_count_text"]? - rv["short_view_count_text"] = rv_params.not_nil!["short_view_count_text"] - rv - else - nil - end - else - rv - end +def get_video(id, db, refresh = true, region = nil, force_refresh = false) + if (video = db.query_one?("SELECT * FROM videos WHERE id = $1", id, as: Video)) && !region + # If record was last updated over 10 minutes ago, or video has since premiered, + # refresh (expire param in response lasts for 6 hours) + if (refresh && + (Time.utc - video.updated > 10.minutes) || + (video.premiere_timestamp.try &.< Time.utc)) || + force_refresh + begin + video = fetch_video(id, region) + db.exec("UPDATE videos SET (id, info, updated) = ($1, $2, $3) WHERE id = $1", video.id, video.info.to_json, video.updated) + rescue ex + db.exec("DELETE FROM videos * WHERE id = $1", id) + raise ex end - params["rvs"] = (rvs.map &.to_s).join(",") - end - end - - html_info = body.match(/ytplayer\.config = (?.*?);ytplayer\.load/).try &.["info"] - - if html_info - JSON.parse(html_info)["args"].as_h.each do |key, value| - params[key] = value.to_s end else - error_message = html.xpath_node(%q(//h1[@id="unavailable-message"])) - if error_message - params["reason"] = error_message.content.strip - elsif body.includes?("To continue with your YouTube experience, please fill out the form below.") || - body.includes?("https://www.google.com/sorry/index") - params["reason"] = "Could not extract video info. Instance is likely blocked." - else - params["reason"] = "Video unavailable." + video = fetch_video(id, region) + if !region + db.exec("INSERT INTO videos VALUES ($1, $2, $3) ON CONFLICT (id) DO NOTHING", video.id, video.info.to_json, video.updated) end end - return params + return video end def fetch_video(id, region) - response = YT_POOL.client(region, &.get("/watch?v=#{id}&gl=US&hl=en&disable_polymer=1&has_verified=1&bpctr=9999999999")) + response = YT_POOL.client(region, &.get("/watch?v=#{id}&gl=US&hl=en&has_verified=1&bpctr=9999999999")) if md = response.headers["location"]?.try &.match(/v=(?[a-zA-Z0-9_-]{11})/) raise VideoRedirect.new(video_id: md["id"]) end - html = XML.parse_html(response.body) - info = extract_player_config(response.body, html) - info["cookie"] = response.cookies.to_h.map { |name, cookie| "#{name}=#{cookie.value}" }.join("; ") - - allowed_regions = html.xpath_node(%q(//meta[@itemprop="regionsAllowed"])).try &.["content"].split(",") - if !allowed_regions || allowed_regions == [""] - allowed_regions = [] of String - end + info = extract_polymer_config(response.body) + info["cookie"] = JSON::Any.new(response.cookies.to_h.transform_values { |v| JSON::Any.new(v.value) }) + allowed_regions = info["microformat"]?.try &.["playerMicroformatRenderer"]["availableCountries"]?.try &.as_a.map &.as_s || [] of String # Check for region-blocks - if info["reason"]? && info["reason"].includes?("your country") + if info["reason"]?.try &.as_s.includes?("your country") bypass_regions = PROXY_LIST.keys & allowed_regions if !bypass_regions.empty? region = bypass_regions[rand(bypass_regions.size)] - response = YT_POOL.client(region, &.get("/watch?v=#{id}&gl=US&hl=en&disable_polymer=1&has_verified=1&bpctr=9999999999")) + response = YT_POOL.client(region, &.get("/watch?v=#{id}&gl=US&hl=en&has_verified=1&bpctr=9999999999")) - html = XML.parse_html(response.body) - info = extract_player_config(response.body, html) - - info["region"] = region if region - info["cookie"] = response.cookies.to_h.map { |name, cookie| "#{name}=#{cookie.value}" }.join("; ") + region_info = extract_polymer_config(response.body) + region_info["region"] = JSON::Any.new(region) if region + region_info["cookie"] = JSON::Any.new(response.cookies.to_h.transform_values { |v| JSON::Any.new(v.value) }) + info = region_info if !region_info["reason"]? end end # Try to pull streams from embed URL if info["reason"]? embed_page = YT_POOL.client &.get("/embed/#{id}").body - sts = embed_page.match(/"sts"\s*:\s*(?\d+)/).try &.["sts"]? - sts ||= "" - embed_info = HTTP::Params.parse(YT_POOL.client &.get("/get_video_info?video_id=#{id}&eurl=https://youtube.googleapis.com/v/#{id}&gl=US&hl=en&disable_polymer=1&sts=#{sts}").body) + sts = embed_page.match(/"sts"\s*:\s*(?\d+)/).try &.["sts"]? || "" + embed_info = HTTP::Params.parse(YT_POOL.client &.get("/get_video_info?html5=1&video_id=#{id}&eurl=https://youtube.googleapis.com/v/#{id}&gl=US&hl=en&sts=#{sts}").body) - if !embed_info["reason"]? - embed_info.each do |key, value| - info[key] = value.to_s + if embed_info["player_response"]? + player_response = JSON.parse(embed_info["player_response"]) + {"captions", "microformat", "playabilityStatus", "streamingData", "videoDetails", "storyboards"}.each do |f| + info[f] = player_response[f] if player_response[f]? end - else - raise info["reason"] end + + initial_data = JSON.parse(embed_info["watch_next_response"]) if embed_info["watch_next_response"]? + + info["relatedVideos"] = initial_data.try &.["playerOverlays"]?.try &.["playerOverlayRenderer"]? + .try &.["endScreen"]?.try &.["watchNextEndScreenRenderer"]?.try &.["results"]?.try &.as_a.compact_map { |r| + parse_related r + }.try { |a| JSON::Any.new(a) } || embed_info["rvs"]?.try &.split(",").map { |r| + r = HTTP::Params.parse(r).to_h + JSON::Any.new(Hash.zip(r.keys, r.values.map { |v| JSON::Any.new(v) })) + }.try { |a| JSON::Any.new(a) } || JSON::Any.new([] of JSON::Any) end - if info["reason"]? && !info["player_response"]? - raise info["reason"] - end - - player_json = JSON.parse(info["player_response"]) - if reason = player_json["playabilityStatus"]?.try &.["reason"]?.try &.as_s - raise reason - end - - title = player_json["videoDetails"]["title"].as_s - author = player_json["videoDetails"]["author"]?.try &.as_s || "" - ucid = player_json["videoDetails"]["channelId"]?.try &.as_s || "" - - info["premium"] = html.xpath_node(%q(.//span[text()="Premium"])) ? "true" : "false" - - views = html.xpath_node(%q(//meta[@itemprop="interactionCount"])) - .try &.["content"].to_i64? || 0_i64 - - likes = html.xpath_node(%q(//button[@title="I like this"]/span)) - .try &.content.delete(",").try &.to_i? || 0 - - dislikes = html.xpath_node(%q(//button[@title="I dislike this"]/span)) - .try &.content.delete(",").try &.to_i? || 0 - - avg_rating = (likes.to_f/(likes.to_f + dislikes.to_f) * 4 + 1) - avg_rating = avg_rating.nan? ? 0.0 : avg_rating - info["avg_rating"] = "#{avg_rating}" - - description_html = html.xpath_node(%q(//p[@id="eow-description"])).try &.to_xml(options: XML::SaveOptions::NO_DECL) || "

" - wilson_score = ci_lower_bound(likes, likes + dislikes) - - published = html.xpath_node(%q(//meta[@itemprop="datePublished"])).try &.["content"] - published ||= Time.utc.to_s("%Y-%m-%d") - published = Time.parse(published, "%Y-%m-%d", Time::Location.local) - - is_family_friendly = html.xpath_node(%q(//meta[@itemprop="isFamilyFriendly"])).try &.["content"] == "True" - is_family_friendly ||= true - - genre = html.xpath_node(%q(//meta[@itemprop="genre"])).try &.["content"] - genre ||= "" - - genre_url = html.xpath_node(%(//ul[contains(@class, "watch-info-tag-list")]/li/a[text()="#{genre}"])).try &.["href"]? - genre_url ||= "" - - # YouTube provides invalid URLs for some genres, so we fix that here - case genre - when "Comedy" - genre_url = "/channel/UCQZ43c4dAA9eXCQuXWu9aTw" - when "Education" - genre_url = "/channel/UCdxpofrI-dO6oYfsqHDHphw" - when "Gaming" - genre_url = "/channel/UCOpNcN46UbXVtpKMrmU4Abg" - when "Movies" - genre_url = "/channel/UClgRkhTL3_hImCAmdLfDE4g" - when "Nonprofits & Activism" - genre_url = "/channel/UCfFyYRYslvuhwMDnx6KjUvw" - when "Trailers" - genre_url = "/channel/UClgRkhTL3_hImCAmdLfDE4g" - else nil # Ignore - end - - license = html.xpath_node(%q(//h4[contains(text(),"License")]/parent::*/ul/li)).try &.content || "" - sub_count_text = html.xpath_node(%q(//span[contains(@class, "subscriber-count")])).try &.["title"]? || "0" - author_thumbnail = html.xpath_node(%(//span[@class="yt-thumb-clip"]/img)).try &.["data-thumb"]?.try &.gsub(/^\/\//, "https://") || "" - - video = Video.new(id, info, Time.utc, title, views, likes, dislikes, wilson_score, published, description_html, - nil, author, ucid, allowed_regions, is_family_friendly, genre, genre_url, license, sub_count_text, author_thumbnail) + raise info["reason"]?.try &.as_s || "" if !info["videoDetails"]? + video = Video.new(id, info, Time.utc) return video end -def itag_to_metadata?(itag : String) - return VIDEO_FORMATS[itag]? +def itag_to_metadata?(itag : JSON::Any) + return VIDEO_FORMATS[itag.to_s]? end def process_continuation(db, query, plid, id) diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr index e9baba2c..0c19fc1b 100644 --- a/src/invidious/views/components/item.ecr +++ b/src/invidious/views/components/item.ecr @@ -85,7 +85,7 @@

- <% if item.responds_to?(:premiere_timestamp) && item.premiere_timestamp && item.premiere_timestamp.not_nil! > Time.utc %> + <% if item.responds_to?(:premiere_timestamp) && item.premiere_timestamp.try &.> Time.utc %>
<%= translate(locale, "Premieres in `x`", recode_date((item.premiere_timestamp.as(Time) - Time.utc).ago, locale)) %>
<% elsif Time.utc - item.published > 1.minute %>
<%= translate(locale, "Shared `x` ago", recode_date(item.published, locale)) %>
@@ -144,7 +144,7 @@

- <% if item.responds_to?(:premiere_timestamp) && item.premiere_timestamp && item.premiere_timestamp.not_nil! > Time.utc %> + <% if item.responds_to?(:premiere_timestamp) && item.premiere_timestamp.try &.> Time.utc %>
<%= translate(locale, "Premieres in `x`", recode_date((item.premiere_timestamp.as(Time) - Time.utc).ago, locale)) %>
<% elsif Time.utc - item.published > 1.minute %>
<%= translate(locale, "Shared `x` ago", recode_date(item.published, locale)) %>
diff --git a/src/invidious/views/components/player.ecr b/src/invidious/views/components/player.ecr index 3c30f69e..6b01d25f 100644 --- a/src/invidious/views/components/player.ecr +++ b/src/invidious/views/components/player.ecr @@ -3,23 +3,23 @@ <% if params.autoplay %>autoplay<% end %> <% if params.video_loop %>loop<% end %> <% if params.controls %>controls<% end %>> - <% if hlsvp && !CONFIG.disabled?("livestreams") %> - + <% if (hlsvp = video.hls_manifest_url) && !CONFIG.disabled?("livestreams") %> + <% else %> <% if params.listen %> <% audio_streams.each_with_index do |fmt, i| %> - <% if params.local %>&local=true<% end %>" type='<%= fmt["type"] %>' label="<%= fmt["bitrate"] %>k" selected="<%= i == 0 ? true : false %>"> + <% if params.local %>&local=true<% end %>" type='<%= fmt["mimeType"] %>' label="<%= fmt["bitrate"] %>k" selected="<%= i == 0 ? true : false %>"> <% end %> <% else %> <% if params.quality == "dash" %> - + <% end %> <% fmt_stream.each_with_index do |fmt, i| %> <% if params.quality %> - <% if params.local %>&local=true<% end %>" type='<%= fmt["type"] %>' label="<%= fmt["label"] %>" selected="<%= params.quality == fmt["label"].split(" - ")[0] %>"> + <% if params.local %>&local=true<% end %>" type='<%= fmt["mimeType"] %>' label="<%= fmt["quality"] %>" selected="<%= params.quality == fmt["quality"] %>"> <% else %> - <% if params.local %>&local=true<% end %>" type='<%= fmt["type"] %>' label="<%= fmt["label"] %>" selected="<%= i == 0 ? true : false %>"> + <% if params.local %>&local=true<% end %>" type='<%= fmt["mimeType"] %>' label="<%= fmt["quality"] %>" selected="<%= i == 0 ? true : false %>"> <% end %> <% end %> <% end %> diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index ae6341e0..9a1e6c32 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -33,8 +33,8 @@ "index" => continuation, "plid" => plid, "length_seconds" => video.length_seconds.to_f, - "play_next" => !rvs.empty? && !plid && params.continue, - "next_video" => rvs.select { |rv| rv["id"]? }[0]?.try &.["id"], + "play_next" => !video.related_videos.empty? && !plid && params.continue, + "next_video" => video.related_videos.select { |rv| rv["id"]? }[0]?.try &.["id"], "youtube_comments_text" => HTML.escape(translate(locale, "View YouTube comments")), "reddit_comments_text" => HTML.escape(translate(locale, "View Reddit comments")), "reddit_permalink_text" => HTML.escape(translate(locale, "View more comments on Reddit")), @@ -72,13 +72,13 @@
<% end %> - <% if !reason.empty? %> + <% if video.reason %>

- <%= reason %> + <%= video.reason %>

- <% elsif video.premiere_timestamp %> + <% elsif video.premiere_timestamp.try &.> Time.utc %>

- <%= translate(locale, "Premieres in `x`", recode_date((video.premiere_timestamp.as(Time) - Time.utc).ago, locale)) %> + <%= video.premiere_timestamp.try { |t| translate(locale, "Premieres in `x`", recode_date((t - Time.utc).ago, locale)) } %>

<% end %>
@@ -137,18 +137,18 @@ <% {"", "youtube", "reddit"}.each do |option| %> - + <% end %> <% end %> @@ -79,7 +79,7 @@ <% preferences.captions.each_with_index do |caption, index| %> <% end %> @@ -119,7 +119,7 @@
@@ -139,7 +139,7 @@
@@ -149,7 +149,7 @@ <% (feed_options.size - 1).times do |index| %> <% end %> @@ -211,7 +211,7 @@
@@ -221,7 +221,7 @@ <% (feed_options.size - 1).times do |index| %> <% end %> From de777907f2835dc79cb1955fa623928ae3a47aaa Mon Sep 17 00:00:00 2001 From: saltycrys <73420320+saltycrys@users.noreply.github.com> Date: Mon, 16 Nov 2020 04:19:41 +0100 Subject: [PATCH 0202/2930] Apply dark theme immediately Themes are now controlled with a class on the body element. If a preference is set the body element will have either "dark-theme" or "light-theme" class. If no preference is set or the preference is empty the class will be "no-theme". "dark-theme" and "light-theme" are handled by darktheme.css and lighttheme.css respectively. "no-theme" is handled by default.css where depending on the value of "prefers-color-scheme" the styles corresponding to "dark-theme" or "light-theme" are applied. Unfortunately this means that both themes are duplicated, once in the theme .css and once in default.css. --- assets/css/darktheme.css | 28 ++++++++------ assets/css/default.css | 65 ++++++++++++++++++++++++++++++++ assets/css/lighttheme.css | 18 ++++++--- assets/js/themes.js | 15 +++++--- src/invidious/views/template.ecr | 7 ++-- 5 files changed, 108 insertions(+), 25 deletions(-) diff --git a/assets/css/darktheme.css b/assets/css/darktheme.css index 92da15b6..45a267cd 100644 --- a/assets/css/darktheme.css +++ b/assets/css/darktheme.css @@ -1,37 +1,43 @@ -a:hover, -a:active { +/* + * Dark theme + * Same as (prefers-color-scheme: dark) in default.css + */ + +.dark-theme a:hover, +.dark-theme a:active { color: rgb(0, 182, 240); } -a { +.dark-theme a { color: #a0a0a0; text-decoration: none; } -body { +body.dark-theme { background-color: rgba(35, 35, 35, 1); color: #f0f0f0; } -.pure-form legend { +.dark-theme .pure-form legend { color: #f0f0f0; } -.pure-menu-heading { +.dark-theme .pure-menu-heading { color: #f0f0f0; } -input, -select, -textarea { +.dark-theme input, +.dark-theme select, +.dark-theme textarea { color: rgba(35, 35, 35, 1); } -.pure-form input[type="file"] { +.dark-theme .pure-form input[type="file"] { color: #f0f0f0; } -.navbar > .searchbar input { +.dark-theme .navbar > .searchbar input { background-color: inherit; color: inherit; } + diff --git a/assets/css/default.css b/assets/css/default.css index b7a77be6..ccdd7660 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -485,3 +485,68 @@ video.video-js { margin-top: -0.81666em; margin-left: -1.5em; } + +/* + * Automatic theme support + * Same as lighttheme.css and darktheme.css + */ + +@media (prefers-color-scheme: light) { + .no-theme a:hover, + .no-theme a:active { + color: #167ac6 !important; + } + + .no-theme a { + color: #61809b; + text-decoration: none; + } + + /* All links that do not fit with the default color goes here */ + .no-theme a:not([data-id]) > .icon, + .no-theme .pure-u-lg-1-5 > .h-box > a[href^="/watch?"], + .no-theme .playlist-restricted > ol > li > a { + color: #303030; + } +} + +@media (prefers-color-scheme: dark) { + .no-theme a:hover, + .no-theme a:active { + color: rgb(0, 182, 240); + } + + .no-theme a { + color: #a0a0a0; + text-decoration: none; + } + + body.no-theme { + background-color: rgba(35, 35, 35, 1); + color: #f0f0f0; + } + + .no-theme .pure-form legend { + color: #f0f0f0; + } + + .no-theme .pure-menu-heading { + color: #f0f0f0; + } + + .no-theme input, + .no-theme select, + .no-theme textarea { + color: rgba(35, 35, 35, 1); + } + + .no-theme .pure-form input[type="file"] { + color: #f0f0f0; + } + + .no-theme .navbar > .searchbar input { + background-color: inherit; + color: inherit; + } +} + diff --git a/assets/css/lighttheme.css b/assets/css/lighttheme.css index 73706bb7..7452180b 100644 --- a/assets/css/lighttheme.css +++ b/assets/css/lighttheme.css @@ -1,16 +1,22 @@ -a:hover, -a:active { +/* + * Light theme + * Same as (prefers-color-scheme: light) in default.css + */ + +.light-theme a:hover, +.light-theme a:active { color: #167ac6 !important; } -a { +.light-theme a { color: #61809b; text-decoration: none; } /* All links that do not fit with the default color goes here */ -a:not([data-id]) > .icon, -.pure-u-lg-1-5 > .h-box > a[href^="/watch?"], -.playlist-restricted > ol > li > a { +.light-theme a:not([data-id]) > .icon, +.light-theme .pure-u-lg-1-5 > .h-box > a[href^="/watch?"], +.light-theme .playlist-restricted > ol > li > a { color: #303030; } + diff --git a/assets/js/themes.js b/assets/js/themes.js index c600073d..543b849e 100644 --- a/assets/js/themes.js +++ b/assets/js/themes.js @@ -2,7 +2,7 @@ var toggle_theme = document.getElementById('toggle_theme'); toggle_theme.href = 'javascript:void(0);'; toggle_theme.addEventListener('click', function () { - var dark_mode = document.getElementById('dark_theme').media === 'none'; + var dark_mode = document.body.classList.contains("light-theme"); var url = '/toggle_theme?redirect=false'; var xhr = new XMLHttpRequest(); @@ -22,7 +22,7 @@ window.addEventListener('storage', function (e) { } }); -window.addEventListener('load', function () { +window.addEventListener('DOMContentLoaded', function () { window.localStorage.setItem('dark_mode', document.getElementById('dark_mode_pref').textContent); // Update localStorage if dark mode preference changed on preferences page update_mode(window.localStorage.dark_mode); @@ -50,13 +50,18 @@ function scheme_switch (e) { } function set_mode (bool) { - document.getElementById('dark_theme').media = !bool ? 'none' : ''; - document.getElementById('light_theme').media = bool ? 'none' : ''; - if (bool) { + // dark toggle_theme.children[0].setAttribute('class', 'icon ion-ios-sunny'); + document.body.classList.remove('no-theme'); + document.body.classList.remove('light-theme'); + document.body.classList.add('dark-theme'); } else { + // light toggle_theme.children[0].setAttribute('class', 'icon ion-ios-moon'); + document.body.classList.remove('no-theme'); + document.body.classList.remove('dark-theme'); + document.body.classList.add('light-theme'); } } diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr index 61cf5c3a..558f896e 100644 --- a/src/invidious/views/template.ecr +++ b/src/invidious/views/template.ecr @@ -18,13 +18,14 @@ - media="none"<% end %>> - media="none"<% end %>> + + <% locale = LOCALES[env.get("preferences").as(Preferences).locale]? %> +<% dark_mode = env.get("preferences").as(Preferences).dark_mode %> - +-theme">
From ff46c1816483472c2b478a570dceb3b75be6e783 Mon Sep 17 00:00:00 2001 From: saltycrys <73420320+saltycrys@users.noreply.github.com> Date: Tue, 17 Nov 2020 22:53:45 +0100 Subject: [PATCH 0203/2930] Move themes into default.css Now that themes are controlled with a class instead of setting media="none" on the stylesheet link and both themes already being duplicated in default.css for the automatic themeing it makes sense to have all theme related CSS in the same place. This commit also fixes the missing dark theme on embeds. --- assets/css/darktheme.css | 43 ---------------------- assets/css/default.css | 62 ++++++++++++++++++++++++++++++-- assets/css/lighttheme.css | 22 ------------ src/invidious/views/embed.ecr | 3 +- src/invidious/views/template.ecr | 2 -- 5 files changed, 61 insertions(+), 71 deletions(-) delete mode 100644 assets/css/darktheme.css delete mode 100644 assets/css/lighttheme.css diff --git a/assets/css/darktheme.css b/assets/css/darktheme.css deleted file mode 100644 index 45a267cd..00000000 --- a/assets/css/darktheme.css +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Dark theme - * Same as (prefers-color-scheme: dark) in default.css - */ - -.dark-theme a:hover, -.dark-theme a:active { - color: rgb(0, 182, 240); -} - -.dark-theme a { - color: #a0a0a0; - text-decoration: none; -} - -body.dark-theme { - background-color: rgba(35, 35, 35, 1); - color: #f0f0f0; -} - -.dark-theme .pure-form legend { - color: #f0f0f0; -} - -.dark-theme .pure-menu-heading { - color: #f0f0f0; -} - -.dark-theme input, -.dark-theme select, -.dark-theme textarea { - color: rgba(35, 35, 35, 1); -} - -.dark-theme .pure-form input[type="file"] { - color: #f0f0f0; -} - -.dark-theme .navbar > .searchbar input { - background-color: inherit; - color: inherit; -} - diff --git a/assets/css/default.css b/assets/css/default.css index ccdd7660..75e16841 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -487,10 +487,26 @@ video.video-js { } /* - * Automatic theme support - * Same as lighttheme.css and darktheme.css + * Light theme */ +.light-theme a:hover, +.light-theme a:active { + color: #167ac6 !important; +} + +.light-theme a { + color: #61809b; + text-decoration: none; +} + +/* All links that do not fit with the default color goes here */ +.light-theme a:not([data-id]) > .icon, +.light-theme .pure-u-lg-1-5 > .h-box > a[href^="/watch?"], +.light-theme .playlist-restricted > ol > li > a { + color: #303030; +} + @media (prefers-color-scheme: light) { .no-theme a:hover, .no-theme a:active { @@ -510,6 +526,48 @@ video.video-js { } } +/* + * Dark theme + */ + +.dark-theme a:hover, +.dark-theme a:active { + color: rgb(0, 182, 240); +} + +.dark-theme a { + color: #a0a0a0; + text-decoration: none; +} + +body.dark-theme { + background-color: rgba(35, 35, 35, 1); + color: #f0f0f0; +} + +.dark-theme .pure-form legend { + color: #f0f0f0; +} + +.dark-theme .pure-menu-heading { + color: #f0f0f0; +} + +.dark-theme input, +.dark-theme select, +.dark-theme textarea { + color: rgba(35, 35, 35, 1); +} + +.dark-theme .pure-form input[type="file"] { + color: #f0f0f0; +} + +.dark-theme .navbar > .searchbar input { + background-color: inherit; + color: inherit; +} + @media (prefers-color-scheme: dark) { .no-theme a:hover, .no-theme a:active { diff --git a/assets/css/lighttheme.css b/assets/css/lighttheme.css deleted file mode 100644 index 7452180b..00000000 --- a/assets/css/lighttheme.css +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Light theme - * Same as (prefers-color-scheme: light) in default.css - */ - -.light-theme a:hover, -.light-theme a:active { - color: #167ac6 !important; -} - -.light-theme a { - color: #61809b; - text-decoration: none; -} - -/* All links that do not fit with the default color goes here */ -.light-theme a:not([data-id]) > .icon, -.light-theme .pure-u-lg-1-5 > .h-box > a[href^="/watch?"], -.light-theme .playlist-restricted > ol > li > a { - color: #303030; -} - diff --git a/src/invidious/views/embed.ecr b/src/invidious/views/embed.ecr index 48dbc55f..dbb86009 100644 --- a/src/invidious/views/embed.ecr +++ b/src/invidious/views/embed.ecr @@ -9,12 +9,11 @@ - <%= HTML.escape(video.title) %> - Invidious - + From 420ceffbb0cebed6c7b7d976828d6619f2aefe9d Mon Sep 17 00:00:00 2001 From: saltycrys <73420320+saltycrys@users.noreply.github.com> Date: Sun, 27 Dec 2020 05:14:33 +0100 Subject: [PATCH 0266/2930] Rename threads to fibers The config and command line options haven't been changed. --- src/invidious/jobs/refresh_channels_job.cr | 16 ++++++++-------- src/invidious/jobs/refresh_feeds_job.cr | 10 +++++----- src/invidious/jobs/subscribe_to_feeds_job.cr | 12 ++++++------ 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/invidious/jobs/refresh_channels_job.cr b/src/invidious/jobs/refresh_channels_job.cr index bbf55ff3..f4dee063 100644 --- a/src/invidious/jobs/refresh_channels_job.cr +++ b/src/invidious/jobs/refresh_channels_job.cr @@ -7,9 +7,9 @@ class Invidious::Jobs::RefreshChannelsJob < Invidious::Jobs::BaseJob end def begin - max_threads = config.channel_threads - lim_threads = max_threads - active_threads = 0 + max_fibers = config.channel_threads + lim_fibers = max_fibers + active_fibers = 0 active_channel = Channel(Bool).new backoff = 1.seconds @@ -19,26 +19,26 @@ class Invidious::Jobs::RefreshChannelsJob < Invidious::Jobs::BaseJob rs.each do id = rs.read(String) - if active_threads >= lim_threads + if active_fibers >= lim_fibers if active_channel.receive - active_threads -= 1 + active_fibers -= 1 end end - active_threads += 1 + active_fibers += 1 spawn do begin logger.trace("RefreshChannelsJob: Fetching channel #{id}") channel = fetch_channel(id, db, config.full_refresh) - lim_threads = max_threads + lim_fibers = max_fibers db.exec("UPDATE channels SET updated = $1, author = $2, deleted = false WHERE id = $3", Time.utc, channel.author, id) rescue ex logger.error("RefreshChannelsJob: #{id} : #{ex.message}") if ex.message == "Deleted or invalid channel" db.exec("UPDATE channels SET updated = $1, deleted = true WHERE id = $2", Time.utc, id) else - lim_threads = 1 + lim_fibers = 1 logger.error("RefreshChannelsJob: #{id} : backing off for #{backoff}s") sleep backoff if backoff < 1.days diff --git a/src/invidious/jobs/refresh_feeds_job.cr b/src/invidious/jobs/refresh_feeds_job.cr index 5dd47639..208569b8 100644 --- a/src/invidious/jobs/refresh_feeds_job.cr +++ b/src/invidious/jobs/refresh_feeds_job.cr @@ -7,8 +7,8 @@ class Invidious::Jobs::RefreshFeedsJob < Invidious::Jobs::BaseJob end def begin - max_threads = config.feed_threads - active_threads = 0 + max_fibers = config.feed_threads + active_fibers = 0 active_channel = Channel(Bool).new loop do @@ -17,13 +17,13 @@ class Invidious::Jobs::RefreshFeedsJob < Invidious::Jobs::BaseJob email = rs.read(String) view_name = "subscriptions_#{sha256(email)}" - if active_threads >= max_threads + if active_fibers >= max_fibers if active_channel.receive - active_threads -= 1 + active_fibers -= 1 end end - active_threads += 1 + active_fibers += 1 spawn do begin # Drop outdated views diff --git a/src/invidious/jobs/subscribe_to_feeds_job.cr b/src/invidious/jobs/subscribe_to_feeds_job.cr index 3bb31299..2255730d 100644 --- a/src/invidious/jobs/subscribe_to_feeds_job.cr +++ b/src/invidious/jobs/subscribe_to_feeds_job.cr @@ -8,12 +8,12 @@ class Invidious::Jobs::SubscribeToFeedsJob < Invidious::Jobs::BaseJob end def begin - max_threads = 1 + max_fibers = 1 if config.use_pubsub_feeds.is_a?(Int32) - max_threads = config.use_pubsub_feeds.as(Int32) + max_fibers = config.use_pubsub_feeds.as(Int32) end - active_threads = 0 + active_fibers = 0 active_channel = Channel(Bool).new loop do @@ -21,13 +21,13 @@ class Invidious::Jobs::SubscribeToFeedsJob < Invidious::Jobs::BaseJob rs.each do ucid = rs.read(String) - if active_threads >= max_threads.as(Int32) + if active_fibers >= max_fibers.as(Int32) if active_channel.receive - active_threads -= 1 + active_fibers -= 1 end end - active_threads += 1 + active_fibers += 1 spawn do begin From c4ef055248781b36c57d99b97e798673a097ea73 Mon Sep 17 00:00:00 2001 From: saltycrys <73420320+saltycrys@users.noreply.github.com> Date: Sun, 27 Dec 2020 05:20:33 +0100 Subject: [PATCH 0267/2930] Add RefreshChannelsJob traces Traces can be enabled with `-l trace`. The problem with subscriptions is that sometimes requests to YouTube never finish. As soon as that happens `channel-threads` times subscriptions stop being refreshed. This is most likely a problem with the lsquick bindings. --- src/invidious.cr | 20 ++++++------- src/invidious/channels.cr | 33 ++++++++++++++++------ src/invidious/jobs/refresh_channels_job.cr | 17 +++++++---- src/invidious/routes/login.cr | 2 +- src/invidious/users.cr | 10 +++---- 5 files changed, 53 insertions(+), 29 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index 91798129..7184145a 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -256,7 +256,7 @@ before_all do |env| headers["Cookie"] = env.request.headers["Cookie"] begin - user, sid = get_user(sid, headers, PG_DB, false) + user, sid = get_user(sid, headers, PG_DB, logger, false) csrf_token = generate_response(sid, { ":authorize_token", ":playlist_ajax", @@ -526,7 +526,7 @@ post "/subscription_ajax" do |env| case action when "action_create_subscription_to_channel" if !user.subscriptions.includes? channel_id - get_channel(channel_id, PG_DB, false, false) + get_channel(channel_id, PG_DB, logger, false, false) PG_DB.exec("UPDATE users SET feed_needs_update = true, subscriptions = array_append(subscriptions, $1) WHERE email = $2", channel_id, email) end when "action_remove_subscriptions" @@ -561,7 +561,7 @@ get "/subscription_manager" do |env| headers = HTTP::Headers.new headers["Cookie"] = env.request.headers["Cookie"] - user, sid = get_user(sid, headers, PG_DB) + user, sid = get_user(sid, headers, PG_DB, logger) end action_takeout = env.params.query["action_takeout"]?.try &.to_i? @@ -685,7 +685,7 @@ post "/data_control" do |env| user.subscriptions += body["subscriptions"].as_a.map { |a| a.as_s } user.subscriptions.uniq! - user.subscriptions = get_batch_channels(user.subscriptions, PG_DB, false, false) + user.subscriptions = get_batch_channels(user.subscriptions, PG_DB, logger, false, false) PG_DB.exec("UPDATE users SET feed_needs_update = true, subscriptions = $1 WHERE email = $2", user.subscriptions, user.email) end @@ -754,7 +754,7 @@ post "/data_control" do |env| end user.subscriptions.uniq! - user.subscriptions = get_batch_channels(user.subscriptions, PG_DB, false, false) + user.subscriptions = get_batch_channels(user.subscriptions, PG_DB, logger, false, false) PG_DB.exec("UPDATE users SET feed_needs_update = true, subscriptions = $1 WHERE email = $2", user.subscriptions, user.email) when "import_freetube" @@ -763,7 +763,7 @@ post "/data_control" do |env| end user.subscriptions.uniq! - user.subscriptions = get_batch_channels(user.subscriptions, PG_DB, false, false) + user.subscriptions = get_batch_channels(user.subscriptions, PG_DB, logger, false, false) PG_DB.exec("UPDATE users SET feed_needs_update = true, subscriptions = $1 WHERE email = $2", user.subscriptions, user.email) when "import_newpipe_subscriptions" @@ -782,7 +782,7 @@ post "/data_control" do |env| end user.subscriptions.uniq! - user.subscriptions = get_batch_channels(user.subscriptions, PG_DB, false, false) + user.subscriptions = get_batch_channels(user.subscriptions, PG_DB, logger, false, false) PG_DB.exec("UPDATE users SET feed_needs_update = true, subscriptions = $1 WHERE email = $2", user.subscriptions, user.email) when "import_newpipe" @@ -801,7 +801,7 @@ post "/data_control" do |env| user.subscriptions += db.query_all("SELECT url FROM subscriptions", as: String).map { |url| url.lchop("https://www.youtube.com/channel/") } user.subscriptions.uniq! - user.subscriptions = get_batch_channels(user.subscriptions, PG_DB, false, false) + user.subscriptions = get_batch_channels(user.subscriptions, PG_DB, logger, false, false) PG_DB.exec("UPDATE users SET feed_needs_update = true, subscriptions = $1 WHERE email = $2", user.subscriptions, user.email) @@ -1197,7 +1197,7 @@ get "/feed/subscriptions" do |env| headers["Cookie"] = env.request.headers["Cookie"] if !user.password - user, sid = get_user(sid, headers, PG_DB) + user, sid = get_user(sid, headers, PG_DB, logger) end max_results = env.params.query["max_results"]?.try &.to_i?.try &.clamp(0, MAX_ITEMS_PER_PAGE) @@ -2811,7 +2811,7 @@ post "/api/v1/auth/subscriptions/:ucid" do |env| ucid = env.params.url["ucid"] if !user.subscriptions.includes? ucid - get_channel(ucid, PG_DB, false, false) + get_channel(ucid, PG_DB, logger, false, false) PG_DB.exec("UPDATE users SET feed_needs_update = true, subscriptions = array_append(subscriptions,$1) WHERE email = $2", ucid, user.email) end diff --git a/src/invidious/channels.cr b/src/invidious/channels.cr index 444a6eda..659dc599 100644 --- a/src/invidious/channels.cr +++ b/src/invidious/channels.cr @@ -144,7 +144,7 @@ class ChannelRedirect < Exception end end -def get_batch_channels(channels, db, refresh = false, pull_all_videos = true, max_threads = 10) +def get_batch_channels(channels, db, logger, refresh = false, pull_all_videos = true, max_threads = 10) finished_channel = Channel(String | Nil).new spawn do @@ -160,7 +160,7 @@ def get_batch_channels(channels, db, refresh = false, pull_all_videos = true, ma active_threads += 1 spawn do begin - get_channel(ucid, db, refresh, pull_all_videos) + get_channel(ucid, db, logger, refresh, pull_all_videos) finished_channel.send(ucid) rescue ex finished_channel.send(nil) @@ -181,10 +181,10 @@ def get_batch_channels(channels, db, refresh = false, pull_all_videos = true, ma return final end -def get_channel(id, db, refresh = true, pull_all_videos = true) +def get_channel(id, db, logger, refresh = true, pull_all_videos = true) if channel = db.query_one?("SELECT * FROM channels WHERE id = $1", id, as: InvidiousChannel) if refresh && Time.utc - channel.updated > 10.minutes - channel = fetch_channel(id, db, pull_all_videos: pull_all_videos) + channel = fetch_channel(id, db, logger, pull_all_videos: pull_all_videos) channel_array = channel.to_a args = arg_array(channel_array) @@ -192,7 +192,7 @@ def get_channel(id, db, refresh = true, pull_all_videos = true) ON CONFLICT (id) DO UPDATE SET author = $2, updated = $3", args: channel_array) end else - channel = fetch_channel(id, db, pull_all_videos: pull_all_videos) + channel = fetch_channel(id, db, logger, pull_all_videos: pull_all_videos) channel_array = channel.to_a args = arg_array(channel_array) @@ -202,8 +202,12 @@ def get_channel(id, db, refresh = true, pull_all_videos = true) return channel end -def fetch_channel(ucid, db, pull_all_videos = true, locale = nil) +def fetch_channel(ucid, db, logger, pull_all_videos = true, locale = nil) + logger.trace("fetch_channel: #{ucid} : pull_all_videos = #{pull_all_videos}, locale = #{locale}") + + logger.trace("fetch_channel: #{ucid} : Downloading RSS feed") rss = YT_POOL.client &.get("/feeds/videos.xml?channel_id=#{ucid}").body + logger.trace("fetch_channel: #{ucid} : Parsing RSS feed") rss = XML.parse_html(rss) author = rss.xpath_node(%q(//feed/title)) @@ -219,14 +223,19 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil) auto_generated = true end + logger.trace("fetch_channel: #{ucid} : author = #{author}, auto_generated = #{auto_generated}") + page = 1 + logger.trace("fetch_channel: #{ucid} : Downloading channel videos page") response = get_channel_videos_response(ucid, page, auto_generated: auto_generated) videos = [] of SearchVideo begin initial_data = JSON.parse(response.body).as_a.find &.["response"]? raise InfoException.new("Could not extract channel JSON") if !initial_data + + logger.trace("fetch_channel: #{ucid} : Extracting videos from channel videos page initial_data") videos = extract_videos(initial_data.as_h, author, ucid) rescue ex if response.body.includes?("To continue with your YouTube experience, please fill out the form below.") || @@ -236,6 +245,7 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil) raise ex end + logger.trace("fetch_channel: #{ucid} : Extracting videos from channel RSS feed") rss.xpath_nodes("//feed/entry").each do |entry| video_id = entry.xpath_node("videoid").not_nil!.content title = entry.xpath_node("title").not_nil!.content @@ -269,6 +279,8 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil) views: views, }) + logger.trace("fetch_channel: #{ucid} : video #{video_id} : Updating or inserting video") + # We don't include the 'premiere_timestamp' here because channel pages don't include them, # meaning the above timestamp is always null was_insert = db.query_one("INSERT INTO channel_videos VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) \ @@ -276,8 +288,13 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil) updated = $4, ucid = $5, author = $6, length_seconds = $7, \ live_now = $8, views = $10 returning (xmax=0) as was_insert", *video.to_tuple, as: Bool) - db.exec("UPDATE users SET notifications = array_append(notifications, $1), \ - feed_needs_update = true WHERE $2 = ANY(subscriptions)", video.id, video.ucid) if was_insert + if was_insert + logger.trace("fetch_channel: #{ucid} : video #{video_id} : Inserted, updating subscriptions") + db.exec("UPDATE users SET notifications = array_append(notifications, $1), \ + feed_needs_update = true WHERE $2 = ANY(subscriptions)", video.id, video.ucid) + else + logger.trace("fetch_channel: #{ucid} : video #{video_id} : Updated") + end end if pull_all_videos diff --git a/src/invidious/jobs/refresh_channels_job.cr b/src/invidious/jobs/refresh_channels_job.cr index f4dee063..6c858afa 100644 --- a/src/invidious/jobs/refresh_channels_job.cr +++ b/src/invidious/jobs/refresh_channels_job.cr @@ -20,18 +20,23 @@ class Invidious::Jobs::RefreshChannelsJob < Invidious::Jobs::BaseJob id = rs.read(String) if active_fibers >= lim_fibers + logger.trace("RefreshChannelsJob: Fiber limit reached, waiting...") if active_channel.receive + logger.trace("RefreshChannelsJob: Fiber limit ok, continuing") active_fibers -= 1 end end + logger.trace("RefreshChannelsJob: #{id} : Spawning fiber") active_fibers += 1 spawn do begin - logger.trace("RefreshChannelsJob: Fetching channel #{id}") - channel = fetch_channel(id, db, config.full_refresh) + logger.trace("RefreshChannelsJob: #{id} fiber : Fetching channel") + channel = fetch_channel(id, db, logger, config.full_refresh) lim_fibers = max_fibers + + logger.trace("RefreshChannelsJob: #{id} fiber : Updating DB") db.exec("UPDATE channels SET updated = $1, author = $2, deleted = false WHERE id = $3", Time.utc, channel.author, id) rescue ex logger.error("RefreshChannelsJob: #{id} : #{ex.message}") @@ -39,7 +44,7 @@ class Invidious::Jobs::RefreshChannelsJob < Invidious::Jobs::BaseJob db.exec("UPDATE channels SET updated = $1, deleted = true WHERE id = $2", Time.utc, id) else lim_fibers = 1 - logger.error("RefreshChannelsJob: #{id} : backing off for #{backoff}s") + logger.error("RefreshChannelsJob: #{id} fiber : backing off for #{backoff}s") sleep backoff if backoff < 1.days backoff += backoff @@ -47,13 +52,15 @@ class Invidious::Jobs::RefreshChannelsJob < Invidious::Jobs::BaseJob backoff = 1.days end end + ensure + logger.trace("RefreshChannelsJob: #{id} fiber : Done") + active_channel.send(true) end - - active_channel.send(true) end end end + logger.debug("RefreshChannelsJob: Done, sleeping for one minute") sleep 1.minute Fiber.yield end diff --git a/src/invidious/routes/login.cr b/src/invidious/routes/login.cr index 45a6d4d8..42fb4676 100644 --- a/src/invidious/routes/login.cr +++ b/src/invidious/routes/login.cr @@ -267,7 +267,7 @@ class Invidious::Routes::Login < Invidious::Routes::BaseRoute raise "Couldn't get SID." end - user, sid = get_user(sid, headers, PG_DB) + user, sid = get_user(sid, headers, PG_DB, logger) # We are now logged in traceback << "done.
" diff --git a/src/invidious/users.cr b/src/invidious/users.cr index 6a3ca5c1..38767a23 100644 --- a/src/invidious/users.cr +++ b/src/invidious/users.cr @@ -269,12 +269,12 @@ struct Preferences end end -def get_user(sid, headers, db, refresh = true) +def get_user(sid, headers, db, logger, refresh = true) if email = db.query_one?("SELECT email FROM session_ids WHERE id = $1", sid, as: String) user = db.query_one("SELECT * FROM users WHERE email = $1", email, as: User) if refresh && Time.utc - user.updated > 1.minute - user, sid = fetch_user(sid, headers, db) + user, sid = fetch_user(sid, headers, db, logger) user_array = user.to_a user_array[4] = user_array[4].to_json # User preferences args = arg_array(user_array) @@ -292,7 +292,7 @@ def get_user(sid, headers, db, refresh = true) end end else - user, sid = fetch_user(sid, headers, db) + user, sid = fetch_user(sid, headers, db, logger) user_array = user.to_a user_array[4] = user_array[4].to_json # User preferences args = arg_array(user.to_a) @@ -313,7 +313,7 @@ def get_user(sid, headers, db, refresh = true) return user, sid end -def fetch_user(sid, headers, db) +def fetch_user(sid, headers, db, logger) feed = YT_POOL.client &.get("/subscription_manager?disable_polymer=1", headers) feed = XML.parse_html(feed.body) @@ -326,7 +326,7 @@ def fetch_user(sid, headers, db) end end - channels = get_batch_channels(channels, db, false, false) + channels = get_batch_channels(channels, db, logger, false, false) email = feed.xpath_node(%q(//a[@class="yt-masthead-picker-header yt-masthead-picker-active-account"])) if email From 198dfffaeb91c03889440a9f8141657bd53734d6 Mon Sep 17 00:00:00 2001 From: saltycrys <73420320+saltycrys@users.noreply.github.com> Date: Sun, 27 Dec 2020 06:12:43 +0100 Subject: [PATCH 0268/2930] Add `popular-enabled` option This is similar to the removed `top-enabled` option but for the Popular feed. The instance needs to be restarted if the feed was enabled. Editing admin options on the preferences page is also fixed. The handling of the feed pages now only happens in a single place. Instead of redirecting: - The Top feed now displays a message that it was removed from Invidious. - The Popular feed now displays a message that it was disabled if it was. --- src/invidious.cr | 25 ++++++++++++++++++++---- src/invidious/helpers/helpers.cr | 3 ++- src/invidious/routes/home.cr | 12 +++--------- src/invidious/routes/user_preferences.cr | 4 ++++ src/invidious/views/message.ecr | 12 ++++++++++++ src/invidious/views/preferences.ecr | 6 ++++++ 6 files changed, 48 insertions(+), 14 deletions(-) create mode 100644 src/invidious/views/message.ecr diff --git a/src/invidious.cr b/src/invidious.cr index 40acd9df..af36e25c 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -162,13 +162,16 @@ end Invidious::Jobs.register Invidious::Jobs::RefreshChannelsJob.new(PG_DB, logger, config) Invidious::Jobs.register Invidious::Jobs::RefreshFeedsJob.new(PG_DB, logger, config) Invidious::Jobs.register Invidious::Jobs::SubscribeToFeedsJob.new(PG_DB, logger, config, HMAC_KEY) -Invidious::Jobs.register Invidious::Jobs::PullPopularVideosJob.new(PG_DB) Invidious::Jobs.register Invidious::Jobs::UpdateDecryptFunctionJob.new if config.statistics_enabled Invidious::Jobs.register Invidious::Jobs::StatisticsRefreshJob.new(PG_DB, config, SOFTWARE) end +if config.popular_enabled + Invidious::Jobs.register Invidious::Jobs::PullPopularVideosJob.new(PG_DB) +end + if config.captcha_key Invidious::Jobs.register Invidious::Jobs::BypassCaptchaJob.new(logger, config) end @@ -1140,13 +1143,20 @@ end get "/feed/top" do |env| locale = LOCALES[env.get("preferences").as(Preferences).locale]? - env.redirect "/" + + message = translate(locale, "The Top feed has been removed from Invidious.") + templated "message" end get "/feed/popular" do |env| locale = LOCALES[env.get("preferences").as(Preferences).locale]? - templated "popular" + if config.popular_enabled + templated "popular" + else + message = translate(locale, "The Popular feed has been disabled by the administrator.") + templated "message" + end end get "/feed/trending" do |env| @@ -2210,6 +2220,12 @@ get "/api/v1/popular" do |env| env.response.content_type = "application/json" + if !config.popular_enabled + error_message = {"error" => "Administrator has disabled this endpoint."}.to_json + env.response.status_code = 400 + next error_message + end + JSON.build do |json| json.array do popular_videos.each do |video| @@ -2223,7 +2239,8 @@ get "/api/v1/top" do |env| locale = LOCALES[env.get("preferences").as(Preferences).locale]? env.response.content_type = "application/json" - "[]" + env.response.status_code = 400 + {"error" => "The Top feed has been removed from Invidious."}.to_json end get "/api/v1/channels/:ucid" do |env| diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr index dc68fb5c..29fd6a86 100644 --- a/src/invidious/helpers/helpers.cr +++ b/src/invidious/helpers/helpers.cr @@ -60,7 +60,7 @@ struct ConfigPreferences end end -struct Config +class Config include YAML::Serializable property channel_threads : Int32 # Number of threads to use for crawling videos from channels (for updating subscriptions) @@ -71,6 +71,7 @@ struct Config 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 diff --git a/src/invidious/routes/home.cr b/src/invidious/routes/home.cr index 9b1bf61b..486a7344 100644 --- a/src/invidious/routes/home.cr +++ b/src/invidious/routes/home.cr @@ -5,30 +5,24 @@ class Invidious::Routes::Home < Invidious::Routes::BaseRoute user = env.get? "user" case preferences.default_home - when "" - templated "empty" when "Popular" - templated "popular" + env.redirect "/feed/popular" when "Trending" env.redirect "/feed/trending" when "Subscriptions" if user env.redirect "/feed/subscriptions" else - templated "popular" + env.redirect "/feed/popular" end when "Playlists" if user env.redirect "/view_all_playlists" else - templated "popular" + env.redirect "/feed/popular" end else templated "empty" end end - - private def popular_videos - Jobs::PullPopularVideosJob::POPULAR_VIDEOS.get - end end diff --git a/src/invidious/routes/user_preferences.cr b/src/invidious/routes/user_preferences.cr index 3e4ec330..a0125ed5 100644 --- a/src/invidious/routes/user_preferences.cr +++ b/src/invidious/routes/user_preferences.cr @@ -154,6 +154,10 @@ class Invidious::Routes::UserPreferences < Invidious::Routes::BaseRoute end config.default_user_preferences.feed_menu = admin_feed_menu + popular_enabled = env.params.body["popular_enabled"]?.try &.as(String) + popular_enabled ||= "off" + config.popular_enabled = popular_enabled == "on" + captcha_enabled = env.params.body["captcha_enabled"]?.try &.as(String) captcha_enabled ||= "off" config.captcha_enabled = captcha_enabled == "on" diff --git a/src/invidious/views/message.ecr b/src/invidious/views/message.ecr new file mode 100644 index 00000000..8c7bf611 --- /dev/null +++ b/src/invidious/views/message.ecr @@ -0,0 +1,12 @@ +<% content_for "header" do %> +"> + + Invidious + +<% end %> + +<%= rendered "components/feed_menu" %> + +

+ <%= message %> +

diff --git a/src/invidious/views/preferences.ecr b/src/invidious/views/preferences.ecr index aebdab9e..c05a902c 100644 --- a/src/invidious/views/preferences.ecr +++ b/src/invidious/views/preferences.ecr @@ -227,6 +227,12 @@ <% end %>
+
+ + checked<% end %>> +
+ +
checked<% end %>> From e0d25ff8878c4e9749dc921839a2277aa29f0ebb Mon Sep 17 00:00:00 2001 From: Andrew Zhao Date: Wed, 23 Dec 2020 00:52:23 -0500 Subject: [PATCH 0269/2930] Close http clients after using The crystal http client maintains a keepalive connection to the other server which stays alive for some time. This should be closed if the client instance is not used again to avoid hogging resources --- src/invidious.cr | 18 ++++++++++-------- src/invidious/comments.cr | 2 ++ src/invidious/helpers/proxy.cr | 6 +++++- src/invidious/helpers/utils.cr | 11 ++++++++++- src/invidious/jobs/bypass_captcha_job.cr | 2 ++ src/invidious/users.cr | 2 +- 6 files changed, 30 insertions(+), 11 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index 91798129..9e6f4797 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -2123,14 +2123,13 @@ get "/api/v1/annotations/:id" do |env| file = URI.encode_www_form("#{id[0, 3]}/#{id}.xml") - client = make_client(ARCHIVE_URL) - location = client.get("/download/youtubeannotations_#{index}/#{id[0, 2]}.tar/#{file}") + location = make_client(ARCHIVE_URL, &.get("/download/youtubeannotations_#{index}/#{id[0, 2]}.tar/#{file}")) if !location.headers["Location"]? env.response.status_code = location.status_code end - response = make_client(URI.parse(location.headers["Location"])).get(location.headers["Location"]) + response = make_client(URI.parse(location.headers["Location"]), &.get(location.headers["Location"])) if response.body.empty? env.response.status_code = 404 @@ -3481,8 +3480,12 @@ get "/videoplayback" do |env| location = URI.parse(response.headers["Location"]) env.response.headers["Access-Control-Allow-Origin"] = "*" - host = "#{location.scheme}://#{location.host}" - client = make_client(URI.parse(host), region) + new_host = "#{location.scheme}://#{location.host}" + if new_host != host + host = new_host + client.close + client = make_client(URI.parse(new_host), region) + end url = "#{location.full_path}&host=#{location.host}#{region ? "®ion=#{region}" : ""}" else @@ -3513,7 +3516,6 @@ get "/videoplayback" do |env| end begin - client = make_client(URI.parse(host), region) client.get(url, headers) do |response| response.headers.each do |key, value| if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase) @@ -3554,8 +3556,6 @@ get "/videoplayback" do |env| chunk_end = chunk_start + HTTP_CHUNK_SIZE - 1 end - client = make_client(URI.parse(host), region) - # TODO: Record bytes written so we can restart after a chunk fails while true if !range_end && content_length @@ -3619,6 +3619,7 @@ get "/videoplayback" do |env| if ex.message != "Error reading socket: Connection reset by peer" break else + client.close client = make_client(URI.parse(host), region) end end @@ -3628,6 +3629,7 @@ get "/videoplayback" do |env| first_chunk = false end end + client.close end get "/ggpht/*" do |env| diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr index 9f9edca0..8849c87f 100644 --- a/src/invidious/comments.cr +++ b/src/invidious/comments.cr @@ -269,6 +269,8 @@ def fetch_reddit_comments(id, sort_by = "confidence") raise InfoException.new("Could not fetch comments") end + client.close + comments = result[1].data.as(RedditListing).children return comments, thread end diff --git a/src/invidious/helpers/proxy.cr b/src/invidious/helpers/proxy.cr index 4f415ba0..7a42ef41 100644 --- a/src/invidious/helpers/proxy.cr +++ b/src/invidious/helpers/proxy.cr @@ -108,7 +108,9 @@ def filter_proxies(proxies) proxy = HTTPProxy.new(proxy_host: proxy[:ip], proxy_port: proxy[:port]) client.set_proxy(proxy) - client.head("/").status_code == 200 + status_ok = client.head("/").status_code == 200 + client.close + status_ok rescue ex false end @@ -132,6 +134,7 @@ def get_nova_proxies(country_code = "US") headers["Referer"] = "https://www.proxynova.com/proxy-server-list/country-#{country_code}/" response = client.get("/proxy-server-list/country-#{country_code}/", headers) + client.close document = XML.parse_html(response.body) proxies = [] of {ip: String, port: Int32, score: Float64} @@ -177,6 +180,7 @@ def get_spys_proxies(country_code = "US") } response = client.post("/free-proxy-list/#{country_code}/", headers, form: body) + client.close 20.times do if response.status_code == 200 break diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index bb9a35ea..f068b5f2 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -101,6 +101,15 @@ def make_client(url : URI, region = nil) return client end +def make_client(url : URI, region = nil, &block) + client = make_client(url, region) + begin + yield client + ensure + client.close + end +end + def decode_length_seconds(string) length_seconds = string.gsub(/[^0-9:]/, "").split(":").map &.to_i length_seconds = [0] * (3 - length_seconds.size) + length_seconds @@ -361,7 +370,7 @@ def subscribe_pubsub(topic, key, config) "hub.secret" => key.to_s, } - return make_client(PUBSUB_URL).post("/subscribe", form: body) + return make_client(PUBSUB_URL, &.post("/subscribe", form: body)) end def parse_range(range) diff --git a/src/invidious/jobs/bypass_captcha_job.cr b/src/invidious/jobs/bypass_captcha_job.cr index daba64d5..61f8eaf3 100644 --- a/src/invidious/jobs/bypass_captcha_job.cr +++ b/src/invidious/jobs/bypass_captcha_job.cr @@ -91,6 +91,8 @@ class Invidious::Jobs::BypassCaptchaJob < Invidious::Jobs::BaseJob }, }.to_json).body) + captcha_client.close + raise response["error"].as_s if response["error"]? task_id = response["taskId"].as_i diff --git a/src/invidious/users.cr b/src/invidious/users.cr index 6a3ca5c1..6bc2acd2 100644 --- a/src/invidious/users.cr +++ b/src/invidious/users.cr @@ -427,7 +427,7 @@ def generate_captcha(key, db) end def generate_text_captcha(key, db) - response = make_client(TEXTCAPTCHA_URL).get("/omarroth@protonmail.com.json").body + response = make_client(TEXTCAPTCHA_URL, &.get("/omarroth@protonmail.com.json").body) response = JSON.parse(response) tokens = response["a"].as_a.map do |answer| From dfd5e3001584c49b662f3a99470d63e88053dfc7 Mon Sep 17 00:00:00 2001 From: saltycrys <73420320+saltycrys@users.noreply.github.com> Date: Tue, 29 Dec 2020 01:22:56 +0100 Subject: [PATCH 0270/2930] Fix Video Mode Button The query params that get edited for `embed_params` are now deep copied instead of shallow copied, preventing the originals from being changed. --- src/invidious/routes/watch.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/routes/watch.cr b/src/invidious/routes/watch.cr index 7225c17f..a5c05c00 100644 --- a/src/invidious/routes/watch.cr +++ b/src/invidious/routes/watch.cr @@ -30,7 +30,7 @@ class Invidious::Routes::Watch < Invidious::Routes::BaseRoute embed_link = "/embed/#{id}" if env.params.query.size > 1 - embed_params = env.params.query.dup + embed_params = HTTP::Params.parse(env.params.query.to_s) embed_params.delete_all("v") embed_link += "?" embed_link += embed_params.to_s From ac0ed14eae9fb90d29959a3b4a7017e4d7a03de4 Mon Sep 17 00:00:00 2001 From: Andrew Zhao Date: Tue, 29 Dec 2020 17:58:24 -0500 Subject: [PATCH 0271/2930] do not add local to xhr when in videoplayback --- assets/js/player.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/player.js b/assets/js/player.js index fcba43d8..6d143ed0 100644 --- a/assets/js/player.js +++ b/assets/js/player.js @@ -42,7 +42,7 @@ var shareOptions = { } videojs.Hls.xhr.beforeRequest = function(options) { - if (options.uri.indexOf('local=true') === -1) { + if (options.uri.indexOf('videoplayback') === -1 && options.uri.indexOf('local=true') === -1) { options.uri = options.uri + '?local=true'; } return options; From 608b9e66f4faa438d38a021c1c1885e789a431e2 Mon Sep 17 00:00:00 2001 From: bopol Date: Wed, 30 Dec 2020 00:27:55 +0100 Subject: [PATCH 0272/2930] fix channel/ID/community endpoint fixes https://github.com/iv-org/invidious/issues/1611 --- src/invidious/channels.cr | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/invidious/channels.cr b/src/invidious/channels.cr index 444a6eda..0f493eec 100644 --- a/src/invidious/channels.cr +++ b/src/invidious/channels.cr @@ -634,7 +634,8 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode) else video_id = attachment["videoId"].as_s - json.field "title", attachment["title"]["simpleText"].as_s + video_title = attachment["title"]["simpleText"]? || attachment["title"]["runs"]?.try &.[0]?.try &.["text"]? + json.field "title", video_title json.field "videoId", video_id json.field "videoThumbnails" do generate_thumbnails(json, video_id) @@ -656,7 +657,7 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode) json.field "published", published.to_unix json.field "publishedText", translate(locale, "`x` ago", recode_date(published, locale)) - view_count = attachment["viewCountText"]["simpleText"].as_s.gsub(/\D/, "").to_i64? || 0_i64 + view_count = attachment["viewCountText"]?.try &.["simpleText"].as_s.gsub(/\D/, "").to_i64? || 0_i64 json.field "viewCount", view_count json.field "viewCountText", translate(locale, "`x` views", number_to_short_text(view_count)) From c5136ca4d65dd97426101aa68aedc697e3f1df6a Mon Sep 17 00:00:00 2001 From: saltycrys <73420320+saltycrys@users.noreply.github.com> Date: Sun, 3 Jan 2021 19:23:54 +0100 Subject: [PATCH 0273/2930] Download liblsquic.a from iv-org/liblsquic-static-alpine This only affects Docker installs. Regular builds still use the binary shipped with `lsquic.cr`. --- docker/Dockerfile | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index d93f2868..ce4cc765 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -4,12 +4,7 @@ WORKDIR /invidious COPY ./shard.yml ./shard.yml COPY ./shard.lock ./shard.lock RUN shards install && \ - # TODO: Document build instructions - # See https://github.com/omarroth/boringssl-alpine/blob/master/APKBUILD, - # https://github.com/omarroth/lsquic-alpine/blob/master/APKBUILD, - # https://github.com/omarroth/lsquic.cr/issues/1#issuecomment-631610081 - # for details building static lib - curl -Lo ./lib/lsquic/src/lsquic/ext/liblsquic.a https://omar.yt/lsquic/liblsquic-v2.18.1.a + curl -Lo ./lib/lsquic/src/lsquic/ext/liblsquic.a https://github.com/iv-org/lsquic-static-alpine/releases/download/v2.18.1/liblsquic.a COPY ./src/ ./src/ # TODO: .git folder is required for building – this is destructive. # See definition of CURRENT_BRANCH, CURRENT_COMMIT and CURRENT_VERSION. From 3a2bd4e928382a4fbb70d1f978912c72b8f889fe Mon Sep 17 00:00:00 2001 From: vhuynh3000 Date: Sun, 27 Sep 2020 10:19:44 -0700 Subject: [PATCH 0274/2930] add config to decrypt on demand instead of polling --- src/invidious.cr | 8 +- src/invidious/helpers/helpers.cr | 1 + src/invidious/helpers/signatures.cr | 100 +++++++++++------- .../jobs/update_decrypt_function_job.cr | 12 +-- src/invidious/videos.cr | 4 +- 5 files changed, 74 insertions(+), 51 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index 8d4c2e58..dd862e23 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -168,7 +168,11 @@ end Invidious::Jobs.register Invidious::Jobs::RefreshChannelsJob.new(PG_DB, logger, config) Invidious::Jobs.register Invidious::Jobs::RefreshFeedsJob.new(PG_DB, logger, config) Invidious::Jobs.register Invidious::Jobs::SubscribeToFeedsJob.new(PG_DB, logger, config, HMAC_KEY) -Invidious::Jobs.register Invidious::Jobs::UpdateDecryptFunctionJob.new + +DECRYPT_FUNCTION = DecryptFunction.new(CONFIG.decrypt_polling) +if config.decrypt_polling + Invidious::Jobs.register Invidious::Jobs::UpdateDecryptFunctionJob.new(logger) +end if config.statistics_enabled Invidious::Jobs.register Invidious::Jobs::StatisticsRefreshJob.new(PG_DB, config, SOFTWARE) @@ -191,8 +195,6 @@ def popular_videos Invidious::Jobs::PullPopularVideosJob::POPULAR_VIDEOS.get end -DECRYPT_FUNCTION = Invidious::Jobs::UpdateDecryptFunctionJob::DECRYPT_FUNCTION - before_all do |env| preferences = begin Preferences.from_json(env.request.cookies["PREFS"]?.try &.value || "{}") diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr index 2da49abb..a6651a31 100644 --- a/src/invidious/helpers/helpers.cr +++ b/src/invidious/helpers/helpers.cr @@ -67,6 +67,7 @@ class Config property channel_threads : Int32 # Number of threads to use for crawling videos from channels (for updating subscriptions) property feed_threads : Int32 # Number of threads to use for updating feeds property db : DBConfig # Database configuration + property decrypt_polling : Bool = true # Use polling to keep decryption function up to date property full_refresh : Bool # 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 diff --git a/src/invidious/helpers/signatures.cr b/src/invidious/helpers/signatures.cr index f811500f..d8b1de65 100644 --- a/src/invidious/helpers/signatures.cr +++ b/src/invidious/helpers/signatures.cr @@ -1,53 +1,73 @@ alias SigProc = Proc(Array(String), Int32, Array(String)) -def fetch_decrypt_function(id = "CvFH_6DNRCY") - document = YT_POOL.client &.get("/watch?v=#{id}&gl=US&hl=en").body - url = document.match(/src="(?\/s\/player\/[^\/]+\/player_ias[^\/]+\/en_US\/base.js)"/).not_nil!["url"] - player = YT_POOL.client &.get(url).body +struct DecryptFunction + @decrypt_function = [] of {SigProc, Int32} + @decrypt_time = Time.monotonic - function_name = player.match(/^(?[^=]+)=function\(\w\){\w=\w\.split\(""\);[^\. ]+\.[^( ]+/m).not_nil!["name"] - function_body = player.match(/^#{Regex.escape(function_name)}=function\(\w\){(?[^}]+)}/m).not_nil!["body"] - function_body = function_body.split(";")[1..-2] + def initialize(@use_polling = true) + end - var_name = function_body[0][0, 2] - var_body = player.delete("\n").match(/var #{Regex.escape(var_name)}={(?(.*?))};/).not_nil!["body"] + def update_decrypt_function + @decrypt_function = fetch_decrypt_function + end - operations = {} of String => SigProc - var_body.split("},").each do |operation| - op_name = operation.match(/^[^:]+/).not_nil![0] - op_body = operation.match(/\{[^}]+/).not_nil![0] + private def fetch_decrypt_function(id = "CvFH_6DNRCY") + document = YT_POOL.client &.get("/watch?v=#{id}&gl=US&hl=en").body + url = document.match(/src="(?\/s\/player\/[^\/]+\/player_ias[^\/]+\/en_US\/base.js)"/).not_nil!["url"] + player = YT_POOL.client &.get(url).body - case op_body - when "{a.reverse()" - operations[op_name] = ->(a : Array(String), b : Int32) { a.reverse } - when "{a.splice(0,b)" - operations[op_name] = ->(a : Array(String), b : Int32) { a.delete_at(0..(b - 1)); a } - else - operations[op_name] = ->(a : Array(String), b : Int32) { c = a[0]; a[0] = a[b % a.size]; a[b % a.size] = c; a } + function_name = player.match(/^(?[^=]+)=function\(\w\){\w=\w\.split\(""\);[^\. ]+\.[^( ]+/m).not_nil!["name"] + function_body = player.match(/^#{Regex.escape(function_name)}=function\(\w\){(?[^}]+)}/m).not_nil!["body"] + function_body = function_body.split(";")[1..-2] + + var_name = function_body[0][0, 2] + var_body = player.delete("\n").match(/var #{Regex.escape(var_name)}={(?(.*?))};/).not_nil!["body"] + + operations = {} of String => SigProc + var_body.split("},").each do |operation| + op_name = operation.match(/^[^:]+/).not_nil![0] + op_body = operation.match(/\{[^}]+/).not_nil![0] + + case op_body + when "{a.reverse()" + operations[op_name] = ->(a : Array(String), b : Int32) { a.reverse } + when "{a.splice(0,b)" + operations[op_name] = ->(a : Array(String), b : Int32) { a.delete_at(0..(b - 1)); a } + else + operations[op_name] = ->(a : Array(String), b : Int32) { c = a[0]; a[0] = a[b % a.size]; a[b % a.size] = c; a } + end end + + decrypt_function = [] of {SigProc, Int32} + function_body.each do |function| + function = function.lchop(var_name).delete("[].") + + op_name = function.match(/[^\(]+/).not_nil![0] + value = function.match(/\(\w,(?[\d]+)\)/).not_nil!["value"].to_i + + decrypt_function << {operations[op_name], value} + end + + return decrypt_function end - decrypt_function = [] of {SigProc, Int32} - function_body.each do |function| - function = function.lchop(var_name).delete("[].") + def decrypt_signature(fmt : Hash(String, JSON::Any)) + return "" if !fmt["s"]? || !fmt["sp"]? - op_name = function.match(/[^\(]+/).not_nil![0] - value = function.match(/\(\w,(?[\d]+)\)/).not_nil!["value"].to_i + sp = fmt["sp"].as_s + sig = fmt["s"].as_s.split("") + if !@use_polling + now = Time.monotonic + if now - @decrypt_time > 60.seconds || @decrypt_function.size == 0 + @decrypt_function = fetch_decrypt_function + @decrypt_time = Time.monotonic + end + end - decrypt_function << {operations[op_name], value} + @decrypt_function.each do |proc, value| + sig = proc.call(sig, value) + end + + return "&#{sp}=#{sig.join("")}" end - - return decrypt_function -end - -def decrypt_signature(fmt : Hash(String, JSON::Any)) - return "" if !fmt["s"]? || !fmt["sp"]? - - sp = fmt["sp"].as_s - sig = fmt["s"].as_s.split("") - DECRYPT_FUNCTION.each do |proc, value| - sig = proc.call(sig, value) - end - - return "&#{sp}=#{sig.join("")}" end diff --git a/src/invidious/jobs/update_decrypt_function_job.cr b/src/invidious/jobs/update_decrypt_function_job.cr index 5332c672..0a6c09c5 100644 --- a/src/invidious/jobs/update_decrypt_function_job.cr +++ b/src/invidious/jobs/update_decrypt_function_job.cr @@ -1,15 +1,15 @@ class Invidious::Jobs::UpdateDecryptFunctionJob < Invidious::Jobs::BaseJob - DECRYPT_FUNCTION = [] of {SigProc, Int32} + private getter logger : Invidious::LogHandler + + def initialize(@logger) + end def begin loop do begin - decrypt_function = fetch_decrypt_function - DECRYPT_FUNCTION.clear - decrypt_function.each { |df| DECRYPT_FUNCTION << df } + DECRYPT_FUNCTION.update_decrypt_function rescue ex - # TODO: Log error - next + logger.error("UpdateDecryptFunctionJob : #{ex.message}") ensure sleep 1.minute Fiber.yield diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index 4a831110..74edc156 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -580,7 +580,7 @@ struct Video s.each do |k, v| fmt[k] = JSON::Any.new(v) end - fmt["url"] = JSON::Any.new("#{fmt["url"]}#{decrypt_signature(fmt)}") + fmt["url"] = JSON::Any.new("#{fmt["url"]}#{DECRYPT_FUNCTION.decrypt_signature(fmt)}") end fmt["url"] = JSON::Any.new("#{fmt["url"]}&host=#{URI.parse(fmt["url"].as_s).host}") @@ -599,7 +599,7 @@ struct Video s.each do |k, v| fmt[k] = JSON::Any.new(v) end - fmt["url"] = JSON::Any.new("#{fmt["url"]}#{decrypt_signature(fmt)}") + fmt["url"] = JSON::Any.new("#{fmt["url"]}#{DECRYPT_FUNCTION.decrypt_signature(fmt)}") end fmt["url"] = JSON::Any.new("#{fmt["url"]}&host=#{URI.parse(fmt["url"].as_s).host}") From 8b56a038b7c30cb41dbaf67dd42566d84787d79f Mon Sep 17 00:00:00 2001 From: saltycrys <73420320+saltycrys@users.noreply.github.com> Date: Mon, 4 Jan 2021 05:24:08 +0100 Subject: [PATCH 0275/2930] Set content type for HTML error helpers This fixes `Unexpected char '<' at 1:1` errors caused by content type mismatch. --- src/invidious/helpers/errors.cr | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/invidious/helpers/errors.cr b/src/invidious/helpers/errors.cr index 4487ff8c..2c62d44b 100644 --- a/src/invidious/helpers/errors.cr +++ b/src/invidious/helpers/errors.cr @@ -26,6 +26,7 @@ def error_template_helper(env : HTTP::Server::Context, config : Config, locale : if exception.is_a?(InfoException) return error_template_helper(env, config, locale, status_code, exception.message || "") end + env.response.content_type = "text/html" env.response.status_code = status_code issue_template = %(Title: `#{exception.message} (#{exception.class})`) issue_template += %(\nDate: `#{Time::Format::ISO_8601_DATE_TIME.format(Time.utc)}`) @@ -43,6 +44,7 @@ def error_template_helper(env : HTTP::Server::Context, config : Config, locale : end def error_template_helper(env : HTTP::Server::Context, config : Config, locale : Hash(String, JSON::Any) | Nil, status_code : Int32, message : String) + env.response.content_type = "text/html" env.response.status_code = status_code error_message = translate(locale, message) return templated "error" From 36e9fb9d6835dc3738e6142d1c91d7a2bd249d35 Mon Sep 17 00:00:00 2001 From: saltycrys <73420320+saltycrys@users.noreply.github.com> Date: Mon, 4 Jan 2021 05:35:59 +0100 Subject: [PATCH 0276/2930] Fix `watch_videos` endpoint Playlists created by `watch_videos` do not have an author which caused a crash previously. --- src/invidious.cr | 2 ++ src/invidious/playlists.cr | 24 ++++++++++++++++-------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index 8d4c2e58..1d2125cb 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -2621,6 +2621,8 @@ end begin playlist = get_playlist(PG_DB, plid, locale) + rescue ex : InfoException + next error_json(404, ex) rescue ex next error_json(404, "Playlist does not exist.") end diff --git a/src/invidious/playlists.cr b/src/invidious/playlists.cr index d5b41caa..25797a36 100644 --- a/src/invidious/playlists.cr +++ b/src/invidious/playlists.cr @@ -365,9 +365,13 @@ def fetch_playlist(plid, locale) end initial_data = extract_initial_data(response.body) - playlist_info = initial_data["sidebar"]?.try &.["playlistSidebarRenderer"]?.try &.["items"]?.try &.[0]["playlistSidebarPrimaryInfoRenderer"]? + playlist_sidebar_renderer = initial_data["sidebar"]?.try &.["playlistSidebarRenderer"]?.try &.["items"]? + raise InfoException.new("Could not extract playlistSidebarRenderer.") if !playlist_sidebar_renderer + + playlist_info = playlist_sidebar_renderer[0]["playlistSidebarPrimaryInfoRenderer"]? raise InfoException.new("Could not extract playlist info") if !playlist_info + title = playlist_info["title"]?.try &.["runs"][0]?.try &.["text"]?.try &.as_s || "" desc_item = playlist_info["description"]? @@ -392,14 +396,18 @@ def fetch_playlist(plid, locale) end end - author_info = initial_data["sidebar"]?.try &.["playlistSidebarRenderer"]?.try &.["items"]?.try &.[1]["playlistSidebarSecondaryInfoRenderer"]? - .try &.["videoOwner"]["videoOwnerRenderer"]? + if playlist_sidebar_renderer.size < 2 + author = "" + author_thumbnail = "" + ucid = "" + else + author_info = playlist_sidebar_renderer[1]["playlistSidebarSecondaryInfoRenderer"]?.try &.["videoOwner"]["videoOwnerRenderer"]? + raise InfoException.new("Could not extract author info") if !author_info - raise InfoException.new("Could not extract author info") if !author_info - - author_thumbnail = author_info["thumbnail"]["thumbnails"][0]["url"]?.try &.as_s || "" - author = author_info["title"]["runs"][0]["text"]?.try &.as_s || "" - ucid = author_info["title"]["runs"][0]["navigationEndpoint"]["browseEndpoint"]["browseId"]?.try &.as_s || "" + author = author_info["title"]["runs"][0]["text"]?.try &.as_s || "" + author_thumbnail = author_info["thumbnail"]["thumbnails"][0]["url"]?.try &.as_s || "" + ucid = author_info["title"]["runs"][0]["navigationEndpoint"]["browseEndpoint"]["browseId"]?.try &.as_s || "" + end return Playlist.new({ title: title, From 7a8620a570f1004f7db3f018f68fd0364f77bef6 Mon Sep 17 00:00:00 2001 From: saltycrys <73420320+saltycrys@users.noreply.github.com> Date: Mon, 4 Jan 2021 16:05:15 +0100 Subject: [PATCH 0277/2930] Add CLI arguments to config file The log level can now be set with `log_level` (accepts ints and strings). The log file can now be set with `output` (also accepts `STDOUT`). --- src/invidious.cr | 20 ++++++++++++-------- src/invidious/helpers/helpers.cr | 8 +++++--- src/invidious/helpers/logger.cr | 16 ++++++++-------- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index 5d19acf1..e32cb896 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -107,8 +107,6 @@ LOCALES = { YT_POOL = QUICPool.new(YT_URL, capacity: CONFIG.pool_size, timeout: 2.0) config = CONFIG -output = STDOUT -loglvl = LogLevel::Debug Kemal.config.extra_options do |parser| parser.banner = "Usage: invidious [arguments]" @@ -128,12 +126,11 @@ Kemal.config.extra_options do |parser| exit end end - parser.on("-o OUTPUT", "--output=OUTPUT", "Redirect output (default: STDOUT)") do |output_arg| - FileUtils.mkdir_p(File.dirname(output_arg)) - output = File.open(output_arg, mode: "a") + parser.on("-o OUTPUT", "--output=OUTPUT", "Redirect output (default: #{config.output})") do |output| + config.output = output end - parser.on("-l LEVEL", "--log-level=LEVEL", "Log level, one of #{LogLevel.values} (default: #{loglvl})") do |loglvl_arg| - loglvl = LogLevel.parse(loglvl_arg) + parser.on("-l LEVEL", "--log-level=LEVEL", "Log level, one of #{LogLevel.values} (default: #{config.log_level})") do |log_level| + config.log_level = LogLevel.parse(log_level) end parser.on("-v", "--version", "Print version") do puts SOFTWARE.to_pretty_json @@ -143,7 +140,14 @@ end Kemal::CLI.new ARGV -logger = Invidious::LogHandler.new(output, loglvl) +if config.output.upcase == "STDOUT" + output = STDOUT +else + FileUtils.mkdir_p(File.dirname(config.output)) + output = File.open(config.output, mode: "a") +end + +logger = Invidious::LogHandler.new(output, config.log_level) # Check table integrity if CONFIG.check_tables diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr index a6651a31..1866f8e5 100644 --- a/src/invidious/helpers/helpers.cr +++ b/src/invidious/helpers/helpers.cr @@ -64,11 +64,13 @@ end class Config include YAML::Serializable - property channel_threads : Int32 # Number of threads to use for crawling videos from channels (for updating subscriptions) - property feed_threads : Int32 # Number of threads to use for updating feeds + 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::Debug # Default log level, valid YAML values are ints and strings, see src/invidious/helpers/logger.cr property db : DBConfig # Database configuration property decrypt_polling : Bool = true # Use polling to keep decryption function up to date - property full_refresh : Bool # Used for crawling channels: threads should check all videos uploaded by a channel + 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 diff --git a/src/invidious/helpers/logger.cr b/src/invidious/helpers/logger.cr index 4e4d7306..7c5b0247 100644 --- a/src/invidious/helpers/logger.cr +++ b/src/invidious/helpers/logger.cr @@ -1,14 +1,14 @@ require "logger" enum LogLevel - All - Trace - Debug - Info - Warn - Error - Fatal - Off + All = 0 + Trace = 1 + Debug = 2 + Info = 3 + Warn = 4 + Error = 5 + Fatal = 6 + Off = 7 end class Invidious::LogHandler < Kemal::BaseLogHandler From 6365ee7487d86d94f385bfab72a2cc3260cb6690 Mon Sep 17 00:00:00 2001 From: saltycrys <73420320+saltycrys@users.noreply.github.com> Date: Mon, 4 Jan 2021 16:51:06 +0100 Subject: [PATCH 0278/2930] Make logger a constant Instead of passing around `logger` there is now the global `LOGGER`. --- src/invidious.cr | 84 +++++++++---------- src/invidious/channels.cr | 32 +++---- src/invidious/helpers/helpers.cr | 24 +++--- src/invidious/jobs/bypass_captcha_job.cr | 5 +- src/invidious/jobs/refresh_channels_job.cr | 25 +++--- src/invidious/jobs/refresh_feeds_job.cr | 13 ++- src/invidious/jobs/subscribe_to_feeds_job.cr | 7 +- .../jobs/update_decrypt_function_job.cr | 7 +- src/invidious/routes/base_route.cr | 3 +- src/invidious/routes/login.cr | 2 +- src/invidious/routes/watch.cr | 2 +- src/invidious/routing.cr | 4 +- src/invidious/users.cr | 10 +-- 13 files changed, 103 insertions(+), 115 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index e32cb896..2b915350 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -106,31 +106,30 @@ LOCALES = { YT_POOL = QUICPool.new(YT_URL, capacity: CONFIG.pool_size, timeout: 2.0) -config = CONFIG - +# CLI Kemal.config.extra_options do |parser| parser.banner = "Usage: invidious [arguments]" - parser.on("-c THREADS", "--channel-threads=THREADS", "Number of threads for refreshing channels (default: #{config.channel_threads})") do |number| + parser.on("-c THREADS", "--channel-threads=THREADS", "Number of threads for refreshing channels (default: #{CONFIG.channel_threads})") do |number| begin - config.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: #{config.feed_threads})") do |number| + parser.on("-f THREADS", "--feed-threads=THREADS", "Number of threads for refreshing feeds (default: #{CONFIG.feed_threads})") do |number| begin - config.feed_threads = number.to_i + CONFIG.feed_threads = number.to_i rescue ex puts "THREADS must be integer" exit end end - parser.on("-o OUTPUT", "--output=OUTPUT", "Redirect output (default: #{config.output})") do |output| - config.output = output + parser.on("-o OUTPUT", "--output=OUTPUT", "Redirect output (default: #{CONFIG.output})") do |output| + CONFIG.output = output end - parser.on("-l LEVEL", "--log-level=LEVEL", "Log level, one of #{LogLevel.values} (default: #{config.log_level})") do |log_level| - config.log_level = LogLevel.parse(log_level) + parser.on("-l LEVEL", "--log-level=LEVEL", "Log level, one of #{LogLevel.values} (default: #{CONFIG.log_level})") do |log_level| + CONFIG.log_level = LogLevel.parse(log_level) end parser.on("-v", "--version", "Print version") do puts SOFTWARE.to_pretty_json @@ -140,42 +139,41 @@ end Kemal::CLI.new ARGV -if config.output.upcase == "STDOUT" - output = STDOUT -else - FileUtils.mkdir_p(File.dirname(config.output)) - output = File.open(config.output, mode: "a") +if CONFIG.output.upcase != "STDOUT" + FileUtils.mkdir_p(File.dirname(CONFIG.output)) end +OUTPUT = CONFIG.output.upcase == "STDOUT" ? STDOUT : File.open(CONFIG.output, mode: "a") +LOGGER = Invidious::LogHandler.new(OUTPUT, CONFIG.log_level) -logger = Invidious::LogHandler.new(output, config.log_level) +config = CONFIG # Check table integrity if CONFIG.check_tables - check_enum(PG_DB, logger, "privacy", PlaylistPrivacy) + check_enum(PG_DB, "privacy", PlaylistPrivacy) - check_table(PG_DB, logger, "channels", InvidiousChannel) - check_table(PG_DB, logger, "channel_videos", ChannelVideo) - check_table(PG_DB, logger, "playlists", InvidiousPlaylist) - check_table(PG_DB, logger, "playlist_videos", PlaylistVideo) - check_table(PG_DB, logger, "nonces", Nonce) - check_table(PG_DB, logger, "session_ids", SessionId) - check_table(PG_DB, logger, "users", User) - check_table(PG_DB, logger, "videos", Video) + check_table(PG_DB, "channels", InvidiousChannel) + check_table(PG_DB, "channel_videos", ChannelVideo) + check_table(PG_DB, "playlists", InvidiousPlaylist) + check_table(PG_DB, "playlist_videos", PlaylistVideo) + check_table(PG_DB, "nonces", Nonce) + check_table(PG_DB, "session_ids", SessionId) + check_table(PG_DB, "users", User) + check_table(PG_DB, "videos", Video) if CONFIG.cache_annotations - check_table(PG_DB, logger, "annotations", Annotation) + check_table(PG_DB, "annotations", Annotation) end end # Start jobs -Invidious::Jobs.register Invidious::Jobs::RefreshChannelsJob.new(PG_DB, logger, config) -Invidious::Jobs.register Invidious::Jobs::RefreshFeedsJob.new(PG_DB, logger, config) -Invidious::Jobs.register Invidious::Jobs::SubscribeToFeedsJob.new(PG_DB, logger, config, HMAC_KEY) +Invidious::Jobs.register Invidious::Jobs::RefreshChannelsJob.new(PG_DB, config) +Invidious::Jobs.register Invidious::Jobs::RefreshFeedsJob.new(PG_DB, config) +Invidious::Jobs.register Invidious::Jobs::SubscribeToFeedsJob.new(PG_DB, config, HMAC_KEY) DECRYPT_FUNCTION = DecryptFunction.new(CONFIG.decrypt_polling) if config.decrypt_polling - Invidious::Jobs.register Invidious::Jobs::UpdateDecryptFunctionJob.new(logger) + Invidious::Jobs.register Invidious::Jobs::UpdateDecryptFunctionJob.new end if config.statistics_enabled @@ -187,7 +185,7 @@ if config.popular_enabled end if config.captcha_key - Invidious::Jobs.register Invidious::Jobs::BypassCaptchaJob.new(logger, config) + Invidious::Jobs.register Invidious::Jobs::BypassCaptchaJob.new(config) end connection_channel = Channel({Bool, Channel(PQ::Notification)}).new(32) @@ -265,7 +263,7 @@ before_all do |env| headers["Cookie"] = env.request.headers["Cookie"] begin - user, sid = get_user(sid, headers, PG_DB, logger, false) + user, sid = get_user(sid, headers, PG_DB, false) csrf_token = generate_response(sid, { ":authorize_token", ":playlist_ajax", @@ -535,7 +533,7 @@ post "/subscription_ajax" do |env| case action when "action_create_subscription_to_channel" if !user.subscriptions.includes? channel_id - get_channel(channel_id, PG_DB, logger, false, false) + get_channel(channel_id, PG_DB, false, false) PG_DB.exec("UPDATE users SET feed_needs_update = true, subscriptions = array_append(subscriptions, $1) WHERE email = $2", channel_id, email) end when "action_remove_subscriptions" @@ -570,7 +568,7 @@ get "/subscription_manager" do |env| headers = HTTP::Headers.new headers["Cookie"] = env.request.headers["Cookie"] - user, sid = get_user(sid, headers, PG_DB, logger) + user, sid = get_user(sid, headers, PG_DB) end action_takeout = env.params.query["action_takeout"]?.try &.to_i? @@ -694,7 +692,7 @@ post "/data_control" do |env| user.subscriptions += body["subscriptions"].as_a.map { |a| a.as_s } user.subscriptions.uniq! - user.subscriptions = get_batch_channels(user.subscriptions, PG_DB, logger, false, false) + user.subscriptions = get_batch_channels(user.subscriptions, PG_DB, false, false) PG_DB.exec("UPDATE users SET feed_needs_update = true, subscriptions = $1 WHERE email = $2", user.subscriptions, user.email) end @@ -763,7 +761,7 @@ post "/data_control" do |env| end user.subscriptions.uniq! - user.subscriptions = get_batch_channels(user.subscriptions, PG_DB, logger, false, false) + user.subscriptions = get_batch_channels(user.subscriptions, PG_DB, false, false) PG_DB.exec("UPDATE users SET feed_needs_update = true, subscriptions = $1 WHERE email = $2", user.subscriptions, user.email) when "import_freetube" @@ -772,7 +770,7 @@ post "/data_control" do |env| end user.subscriptions.uniq! - user.subscriptions = get_batch_channels(user.subscriptions, PG_DB, logger, false, false) + user.subscriptions = get_batch_channels(user.subscriptions, PG_DB, false, false) PG_DB.exec("UPDATE users SET feed_needs_update = true, subscriptions = $1 WHERE email = $2", user.subscriptions, user.email) when "import_newpipe_subscriptions" @@ -791,7 +789,7 @@ post "/data_control" do |env| end user.subscriptions.uniq! - user.subscriptions = get_batch_channels(user.subscriptions, PG_DB, logger, false, false) + user.subscriptions = get_batch_channels(user.subscriptions, PG_DB, false, false) PG_DB.exec("UPDATE users SET feed_needs_update = true, subscriptions = $1 WHERE email = $2", user.subscriptions, user.email) when "import_newpipe" @@ -810,7 +808,7 @@ post "/data_control" do |env| user.subscriptions += db.query_all("SELECT url FROM subscriptions", as: String).map { |url| url.lchop("https://www.youtube.com/channel/") } user.subscriptions.uniq! - user.subscriptions = get_batch_channels(user.subscriptions, PG_DB, logger, false, false) + user.subscriptions = get_batch_channels(user.subscriptions, PG_DB, false, false) PG_DB.exec("UPDATE users SET feed_needs_update = true, subscriptions = $1 WHERE email = $2", user.subscriptions, user.email) @@ -1213,7 +1211,7 @@ get "/feed/subscriptions" do |env| headers["Cookie"] = env.request.headers["Cookie"] if !user.password - user, sid = get_user(sid, headers, PG_DB, logger) + user, sid = get_user(sid, headers, PG_DB) end max_results = env.params.query["max_results"]?.try &.to_i?.try &.clamp(0, MAX_ITEMS_PER_PAGE) @@ -1517,7 +1515,7 @@ post "/feed/webhook/:token" do |env| signature = env.request.headers["X-Hub-Signature"].lchop("sha1=") if signature != OpenSSL::HMAC.hexdigest(:sha1, HMAC_KEY, body) - logger.error("/feed/webhook/#{token} : Invalid signature") + LOGGER.error("/feed/webhook/#{token} : Invalid signature") env.response.status_code = 200 next end @@ -2835,7 +2833,7 @@ post "/api/v1/auth/subscriptions/:ucid" do |env| ucid = env.params.url["ucid"] if !user.subscriptions.includes? ucid - get_channel(ucid, PG_DB, logger, false, false) + get_channel(ucid, PG_DB, false, false) PG_DB.exec("UPDATE users SET feed_needs_update = true, subscriptions = array_append(subscriptions,$1) WHERE email = $2", ucid, user.email) end @@ -3929,7 +3927,7 @@ add_context_storage_type(Array(String)) add_context_storage_type(Preferences) add_context_storage_type(User) -Kemal.config.logger = logger +Kemal.config.logger = LOGGER Kemal.config.host_binding = Kemal.config.host_binding != "0.0.0.0" ? Kemal.config.host_binding : CONFIG.host_binding Kemal.config.port = Kemal.config.port != 3000 ? Kemal.config.port : CONFIG.port Kemal.run diff --git a/src/invidious/channels.cr b/src/invidious/channels.cr index 6907ff3d..274e86f9 100644 --- a/src/invidious/channels.cr +++ b/src/invidious/channels.cr @@ -144,7 +144,7 @@ class ChannelRedirect < Exception end end -def get_batch_channels(channels, db, logger, refresh = false, pull_all_videos = true, max_threads = 10) +def get_batch_channels(channels, db, refresh = false, pull_all_videos = true, max_threads = 10) finished_channel = Channel(String | Nil).new spawn do @@ -160,7 +160,7 @@ def get_batch_channels(channels, db, logger, refresh = false, pull_all_videos = active_threads += 1 spawn do begin - get_channel(ucid, db, logger, refresh, pull_all_videos) + get_channel(ucid, db, refresh, pull_all_videos) finished_channel.send(ucid) rescue ex finished_channel.send(nil) @@ -181,10 +181,10 @@ def get_batch_channels(channels, db, logger, refresh = false, pull_all_videos = return final end -def get_channel(id, db, logger, refresh = true, pull_all_videos = true) +def get_channel(id, db, refresh = true, pull_all_videos = true) if channel = db.query_one?("SELECT * FROM channels WHERE id = $1", id, as: InvidiousChannel) if refresh && Time.utc - channel.updated > 10.minutes - channel = fetch_channel(id, db, logger, pull_all_videos: pull_all_videos) + channel = fetch_channel(id, db, pull_all_videos: pull_all_videos) channel_array = channel.to_a args = arg_array(channel_array) @@ -192,7 +192,7 @@ def get_channel(id, db, logger, refresh = true, pull_all_videos = true) ON CONFLICT (id) DO UPDATE SET author = $2, updated = $3", args: channel_array) end else - channel = fetch_channel(id, db, logger, pull_all_videos: pull_all_videos) + channel = fetch_channel(id, db, pull_all_videos: pull_all_videos) channel_array = channel.to_a args = arg_array(channel_array) @@ -202,12 +202,12 @@ def get_channel(id, db, logger, refresh = true, pull_all_videos = true) return channel end -def fetch_channel(ucid, db, logger, pull_all_videos = true, locale = nil) - logger.trace("fetch_channel: #{ucid} : pull_all_videos = #{pull_all_videos}, locale = #{locale}") +def fetch_channel(ucid, db, pull_all_videos = true, locale = nil) + LOGGER.trace("fetch_channel: #{ucid} : pull_all_videos = #{pull_all_videos}, locale = #{locale}") - logger.trace("fetch_channel: #{ucid} : Downloading RSS feed") + LOGGER.trace("fetch_channel: #{ucid} : Downloading RSS feed") rss = YT_POOL.client &.get("/feeds/videos.xml?channel_id=#{ucid}").body - logger.trace("fetch_channel: #{ucid} : Parsing RSS feed") + LOGGER.trace("fetch_channel: #{ucid} : Parsing RSS feed") rss = XML.parse_html(rss) author = rss.xpath_node(%q(//feed/title)) @@ -223,11 +223,11 @@ def fetch_channel(ucid, db, logger, pull_all_videos = true, locale = nil) auto_generated = true end - logger.trace("fetch_channel: #{ucid} : author = #{author}, auto_generated = #{auto_generated}") + LOGGER.trace("fetch_channel: #{ucid} : author = #{author}, auto_generated = #{auto_generated}") page = 1 - logger.trace("fetch_channel: #{ucid} : Downloading channel videos page") + LOGGER.trace("fetch_channel: #{ucid} : Downloading channel videos page") response = get_channel_videos_response(ucid, page, auto_generated: auto_generated) videos = [] of SearchVideo @@ -235,7 +235,7 @@ def fetch_channel(ucid, db, logger, pull_all_videos = true, locale = nil) initial_data = JSON.parse(response.body).as_a.find &.["response"]? raise InfoException.new("Could not extract channel JSON") if !initial_data - logger.trace("fetch_channel: #{ucid} : Extracting videos from channel videos page initial_data") + LOGGER.trace("fetch_channel: #{ucid} : Extracting videos from channel videos page initial_data") videos = extract_videos(initial_data.as_h, author, ucid) rescue ex if response.body.includes?("To continue with your YouTube experience, please fill out the form below.") || @@ -245,7 +245,7 @@ def fetch_channel(ucid, db, logger, pull_all_videos = true, locale = nil) raise ex end - logger.trace("fetch_channel: #{ucid} : Extracting videos from channel RSS feed") + LOGGER.trace("fetch_channel: #{ucid} : Extracting videos from channel RSS feed") rss.xpath_nodes("//feed/entry").each do |entry| video_id = entry.xpath_node("videoid").not_nil!.content title = entry.xpath_node("title").not_nil!.content @@ -279,7 +279,7 @@ def fetch_channel(ucid, db, logger, pull_all_videos = true, locale = nil) views: views, }) - logger.trace("fetch_channel: #{ucid} : video #{video_id} : Updating or inserting video") + LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Updating or inserting video") # We don't include the 'premiere_timestamp' here because channel pages don't include them, # meaning the above timestamp is always null @@ -289,11 +289,11 @@ def fetch_channel(ucid, db, logger, pull_all_videos = true, locale = nil) live_now = $8, views = $10 returning (xmax=0) as was_insert", *video.to_tuple, as: Bool) if was_insert - logger.trace("fetch_channel: #{ucid} : video #{video_id} : Inserted, updating subscriptions") + LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Inserted, updating subscriptions") db.exec("UPDATE users SET notifications = array_append(notifications, $1), \ feed_needs_update = true WHERE $2 = ANY(subscriptions)", video.id, video.ucid) else - logger.trace("fetch_channel: #{ucid} : video #{video_id} : Updated") + LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Updated") end end diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr index 1866f8e5..cf965dad 100644 --- a/src/invidious/helpers/helpers.cr +++ b/src/invidious/helpers/helpers.cr @@ -336,11 +336,11 @@ def extract_items(initial_data : Hash(String, JSON::Any), author_fallback : Stri items end -def check_enum(db, logger, enum_name, struct_type = nil) +def check_enum(db, enum_name, struct_type = nil) return # TODO if !db.query_one?("SELECT true FROM pg_type WHERE typname = $1", enum_name, as: Bool) - logger.info("check_enum: CREATE TYPE #{enum_name}") + LOGGER.info("check_enum: CREATE TYPE #{enum_name}") db.using_connection do |conn| conn.as(PG::Connection).exec_all(File.read("config/sql/#{enum_name}.sql")) @@ -348,12 +348,12 @@ def check_enum(db, logger, enum_name, struct_type = nil) end end -def check_table(db, logger, table_name, struct_type = nil) +def check_table(db, table_name, struct_type = nil) # Create table if it doesn't exist begin db.exec("SELECT * FROM #{table_name} LIMIT 0") rescue ex - logger.info("check_table: check_table: CREATE TABLE #{table_name}") + LOGGER.info("check_table: check_table: CREATE TABLE #{table_name}") db.using_connection do |conn| conn.as(PG::Connection).exec_all(File.read("config/sql/#{table_name}.sql")) @@ -373,7 +373,7 @@ def check_table(db, logger, table_name, struct_type = nil) if name != column_array[i]? if !column_array[i]? new_column = column_types.select { |line| line.starts_with? name }[0] - logger.info("check_table: ALTER TABLE #{table_name} ADD COLUMN #{new_column}") + LOGGER.info("check_table: ALTER TABLE #{table_name} ADD COLUMN #{new_column}") db.exec("ALTER TABLE #{table_name} ADD COLUMN #{new_column}") next end @@ -391,29 +391,29 @@ def check_table(db, logger, table_name, struct_type = nil) # There's a column we didn't expect if !new_column - logger.info("check_table: ALTER TABLE #{table_name} DROP COLUMN #{column_array[i]}") + LOGGER.info("check_table: ALTER TABLE #{table_name} DROP COLUMN #{column_array[i]}") db.exec("ALTER TABLE #{table_name} DROP COLUMN #{column_array[i]} CASCADE") column_array = get_column_array(db, table_name) next end - logger.info("check_table: ALTER TABLE #{table_name} ADD COLUMN #{new_column}") + LOGGER.info("check_table: ALTER TABLE #{table_name} ADD COLUMN #{new_column}") db.exec("ALTER TABLE #{table_name} ADD COLUMN #{new_column}") - logger.info("check_table: UPDATE #{table_name} SET #{column_array[i]}_new=#{column_array[i]}") + LOGGER.info("check_table: UPDATE #{table_name} SET #{column_array[i]}_new=#{column_array[i]}") db.exec("UPDATE #{table_name} SET #{column_array[i]}_new=#{column_array[i]}") - logger.info("check_table: ALTER TABLE #{table_name} DROP COLUMN #{column_array[i]} CASCADE") + LOGGER.info("check_table: ALTER TABLE #{table_name} DROP COLUMN #{column_array[i]} CASCADE") db.exec("ALTER TABLE #{table_name} DROP COLUMN #{column_array[i]} CASCADE") - logger.info("check_table: ALTER TABLE #{table_name} RENAME COLUMN #{column_array[i]}_new TO #{column_array[i]}") + LOGGER.info("check_table: ALTER TABLE #{table_name} RENAME COLUMN #{column_array[i]}_new TO #{column_array[i]}") db.exec("ALTER TABLE #{table_name} RENAME COLUMN #{column_array[i]}_new TO #{column_array[i]}") column_array = get_column_array(db, table_name) end else - logger.info("check_table: ALTER TABLE #{table_name} DROP COLUMN #{column_array[i]} CASCADE") + LOGGER.info("check_table: ALTER TABLE #{table_name} DROP COLUMN #{column_array[i]} CASCADE") db.exec("ALTER TABLE #{table_name} DROP COLUMN #{column_array[i]} CASCADE") end end @@ -423,7 +423,7 @@ def check_table(db, logger, table_name, struct_type = nil) column_array.each do |column| if !struct_array.includes? column - logger.info("check_table: ALTER TABLE #{table_name} DROP COLUMN #{column} CASCADE") + LOGGER.info("check_table: ALTER TABLE #{table_name} DROP COLUMN #{column} CASCADE") db.exec("ALTER TABLE #{table_name} DROP COLUMN #{column} CASCADE") end end diff --git a/src/invidious/jobs/bypass_captcha_job.cr b/src/invidious/jobs/bypass_captcha_job.cr index 61f8eaf3..22c54036 100644 --- a/src/invidious/jobs/bypass_captcha_job.cr +++ b/src/invidious/jobs/bypass_captcha_job.cr @@ -1,8 +1,7 @@ class Invidious::Jobs::BypassCaptchaJob < Invidious::Jobs::BaseJob - private getter logger : Invidious::LogHandler private getter config : Config - def initialize(@logger, @config) + def initialize(@config) end def begin @@ -127,7 +126,7 @@ class Invidious::Jobs::BypassCaptchaJob < Invidious::Jobs::BaseJob end end rescue ex - logger.error("BypassCaptchaJob: #{ex.message}") + LOGGER.error("BypassCaptchaJob: #{ex.message}") ensure sleep 1.minute Fiber.yield diff --git a/src/invidious/jobs/refresh_channels_job.cr b/src/invidious/jobs/refresh_channels_job.cr index 6c858afa..0c6ef79e 100644 --- a/src/invidious/jobs/refresh_channels_job.cr +++ b/src/invidious/jobs/refresh_channels_job.cr @@ -1,9 +1,8 @@ class Invidious::Jobs::RefreshChannelsJob < Invidious::Jobs::BaseJob private getter db : DB::Database - private getter logger : Invidious::LogHandler private getter config : Config - def initialize(@db, @logger, @config) + def initialize(@db, @config) end def begin @@ -14,37 +13,37 @@ class Invidious::Jobs::RefreshChannelsJob < Invidious::Jobs::BaseJob backoff = 1.seconds loop do - logger.debug("RefreshChannelsJob: Refreshing all channels") + LOGGER.debug("RefreshChannelsJob: Refreshing all channels") db.query("SELECT id FROM channels ORDER BY updated") do |rs| rs.each do id = rs.read(String) if active_fibers >= lim_fibers - logger.trace("RefreshChannelsJob: Fiber limit reached, waiting...") + LOGGER.trace("RefreshChannelsJob: Fiber limit reached, waiting...") if active_channel.receive - logger.trace("RefreshChannelsJob: Fiber limit ok, continuing") + LOGGER.trace("RefreshChannelsJob: Fiber limit ok, continuing") active_fibers -= 1 end end - logger.trace("RefreshChannelsJob: #{id} : Spawning fiber") + LOGGER.trace("RefreshChannelsJob: #{id} : Spawning fiber") active_fibers += 1 spawn do begin - logger.trace("RefreshChannelsJob: #{id} fiber : Fetching channel") - channel = fetch_channel(id, db, logger, config.full_refresh) + LOGGER.trace("RefreshChannelsJob: #{id} fiber : Fetching channel") + channel = fetch_channel(id, db, config.full_refresh) lim_fibers = max_fibers - logger.trace("RefreshChannelsJob: #{id} fiber : Updating DB") + LOGGER.trace("RefreshChannelsJob: #{id} fiber : Updating DB") db.exec("UPDATE channels SET updated = $1, author = $2, deleted = false WHERE id = $3", Time.utc, channel.author, id) rescue ex - logger.error("RefreshChannelsJob: #{id} : #{ex.message}") + LOGGER.error("RefreshChannelsJob: #{id} : #{ex.message}") if ex.message == "Deleted or invalid channel" db.exec("UPDATE channels SET updated = $1, deleted = true WHERE id = $2", Time.utc, id) else lim_fibers = 1 - logger.error("RefreshChannelsJob: #{id} fiber : backing off for #{backoff}s") + LOGGER.error("RefreshChannelsJob: #{id} fiber : backing off for #{backoff}s") sleep backoff if backoff < 1.days backoff += backoff @@ -53,14 +52,14 @@ class Invidious::Jobs::RefreshChannelsJob < Invidious::Jobs::BaseJob end end ensure - logger.trace("RefreshChannelsJob: #{id} fiber : Done") + LOGGER.trace("RefreshChannelsJob: #{id} fiber : Done") active_channel.send(true) end end end end - logger.debug("RefreshChannelsJob: Done, sleeping for one minute") + LOGGER.debug("RefreshChannelsJob: Done, sleeping for one minute") sleep 1.minute Fiber.yield end diff --git a/src/invidious/jobs/refresh_feeds_job.cr b/src/invidious/jobs/refresh_feeds_job.cr index 208569b8..7b4ccdea 100644 --- a/src/invidious/jobs/refresh_feeds_job.cr +++ b/src/invidious/jobs/refresh_feeds_job.cr @@ -1,9 +1,8 @@ class Invidious::Jobs::RefreshFeedsJob < Invidious::Jobs::BaseJob private getter db : DB::Database - private getter logger : Invidious::LogHandler private getter config : Config - def initialize(@db, @logger, @config) + def initialize(@db, @config) end def begin @@ -30,14 +29,14 @@ class Invidious::Jobs::RefreshFeedsJob < Invidious::Jobs::BaseJob column_array = get_column_array(db, view_name) ChannelVideo.type_array.each_with_index do |name, i| if name != column_array[i]? - logger.info("RefreshFeedsJob: DROP MATERIALIZED VIEW #{view_name}") + LOGGER.info("RefreshFeedsJob: DROP MATERIALIZED VIEW #{view_name}") db.exec("DROP MATERIALIZED VIEW #{view_name}") raise "view does not exist" end end if !db.query_one("SELECT pg_get_viewdef('#{view_name}')", as: String).includes? "WHERE ((cv.ucid = ANY (u.subscriptions))" - logger.info("RefreshFeedsJob: Materialized view #{view_name} is out-of-date, recreating...") + LOGGER.info("RefreshFeedsJob: Materialized view #{view_name} is out-of-date, recreating...") db.exec("DROP MATERIALIZED VIEW #{view_name}") end @@ -49,18 +48,18 @@ class Invidious::Jobs::RefreshFeedsJob < Invidious::Jobs::BaseJob legacy_view_name = "subscriptions_#{sha256(email)[0..7]}" db.exec("SELECT * FROM #{legacy_view_name} LIMIT 0") - logger.info("RefreshFeedsJob: RENAME MATERIALIZED VIEW #{legacy_view_name}") + LOGGER.info("RefreshFeedsJob: RENAME MATERIALIZED VIEW #{legacy_view_name}") db.exec("ALTER MATERIALIZED VIEW #{legacy_view_name} RENAME TO #{view_name}") rescue ex begin # While iterating through, we may have an email stored from a deleted account if db.query_one?("SELECT true FROM users WHERE email = $1", email, as: Bool) - logger.info("RefreshFeedsJob: CREATE #{view_name}") + LOGGER.info("RefreshFeedsJob: CREATE #{view_name}") db.exec("CREATE MATERIALIZED VIEW #{view_name} AS #{MATERIALIZED_VIEW_SQL.call(email)}") db.exec("UPDATE users SET feed_needs_update = false WHERE email = $1", email) end rescue ex - logger.error("RefreshFeedJobs: REFRESH #{email} : #{ex.message}") + LOGGER.error("RefreshFeedJobs: REFRESH #{email} : #{ex.message}") end end end diff --git a/src/invidious/jobs/subscribe_to_feeds_job.cr b/src/invidious/jobs/subscribe_to_feeds_job.cr index 2255730d..750aceb8 100644 --- a/src/invidious/jobs/subscribe_to_feeds_job.cr +++ b/src/invidious/jobs/subscribe_to_feeds_job.cr @@ -1,10 +1,9 @@ class Invidious::Jobs::SubscribeToFeedsJob < Invidious::Jobs::BaseJob private getter db : DB::Database - private getter logger : Invidious::LogHandler private getter hmac_key : String private getter config : Config - def initialize(@db, @logger, @config, @hmac_key) + def initialize(@db, @config, @hmac_key) end def begin @@ -34,10 +33,10 @@ class Invidious::Jobs::SubscribeToFeedsJob < Invidious::Jobs::BaseJob response = subscribe_pubsub(ucid, hmac_key, config) if response.status_code >= 400 - logger.error("SubscribeToFeedsJob: #{ucid} : #{response.body}") + LOGGER.error("SubscribeToFeedsJob: #{ucid} : #{response.body}") end rescue ex - logger.error("SubscribeToFeedsJob: #{ucid} : #{ex.message}") + LOGGER.error("SubscribeToFeedsJob: #{ucid} : #{ex.message}") end active_channel.send(true) diff --git a/src/invidious/jobs/update_decrypt_function_job.cr b/src/invidious/jobs/update_decrypt_function_job.cr index 0a6c09c5..6fa0ae1b 100644 --- a/src/invidious/jobs/update_decrypt_function_job.cr +++ b/src/invidious/jobs/update_decrypt_function_job.cr @@ -1,15 +1,10 @@ class Invidious::Jobs::UpdateDecryptFunctionJob < Invidious::Jobs::BaseJob - private getter logger : Invidious::LogHandler - - def initialize(@logger) - end - def begin loop do begin DECRYPT_FUNCTION.update_decrypt_function rescue ex - logger.error("UpdateDecryptFunctionJob : #{ex.message}") + LOGGER.error("UpdateDecryptFunctionJob : #{ex.message}") ensure sleep 1.minute Fiber.yield diff --git a/src/invidious/routes/base_route.cr b/src/invidious/routes/base_route.cr index 2852cb04..37624267 100644 --- a/src/invidious/routes/base_route.cr +++ b/src/invidious/routes/base_route.cr @@ -1,7 +1,6 @@ abstract class Invidious::Routes::BaseRoute private getter config : Config - private getter logger : Invidious::LogHandler - def initialize(@config, @logger) + def initialize(@config) end end diff --git a/src/invidious/routes/login.cr b/src/invidious/routes/login.cr index 42fb4676..45a6d4d8 100644 --- a/src/invidious/routes/login.cr +++ b/src/invidious/routes/login.cr @@ -267,7 +267,7 @@ class Invidious::Routes::Login < Invidious::Routes::BaseRoute raise "Couldn't get SID." end - user, sid = get_user(sid, headers, PG_DB, logger) + user, sid = get_user(sid, headers, PG_DB) # We are now logged in traceback << "done.
" diff --git a/src/invidious/routes/watch.cr b/src/invidious/routes/watch.cr index a5c05c00..65604a88 100644 --- a/src/invidious/routes/watch.cr +++ b/src/invidious/routes/watch.cr @@ -62,7 +62,7 @@ class Invidious::Routes::Watch < Invidious::Routes::BaseRoute rescue ex : VideoRedirect return env.redirect env.request.resource.gsub(id, ex.video_id) rescue ex - logger.error("get_video: #{id} : #{ex.message}") + LOGGER.error("get_video: #{id} : #{ex.message}") return error_template(500, ex) end diff --git a/src/invidious/routing.cr b/src/invidious/routing.cr index 602e6ae5..593c7372 100644 --- a/src/invidious/routing.cr +++ b/src/invidious/routing.cr @@ -1,14 +1,14 @@ module Invidious::Routing macro get(path, controller, method = :handle) get {{ path }} do |env| - controller_instance = {{ controller }}.new(config, logger) + controller_instance = {{ controller }}.new(config) controller_instance.{{ method.id }}(env) end end macro post(path, controller, method = :handle) post {{ path }} do |env| - controller_instance = {{ controller }}.new(config, logger) + controller_instance = {{ controller }}.new(config) controller_instance.{{ method.id }}(env) end end diff --git a/src/invidious/users.cr b/src/invidious/users.cr index 5dc16edd..6bc2acd2 100644 --- a/src/invidious/users.cr +++ b/src/invidious/users.cr @@ -269,12 +269,12 @@ struct Preferences end end -def get_user(sid, headers, db, logger, 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) user = db.query_one("SELECT * FROM users WHERE email = $1", email, as: User) if refresh && Time.utc - user.updated > 1.minute - user, sid = fetch_user(sid, headers, db, logger) + user, sid = fetch_user(sid, headers, db) user_array = user.to_a user_array[4] = user_array[4].to_json # User preferences args = arg_array(user_array) @@ -292,7 +292,7 @@ def get_user(sid, headers, db, logger, refresh = true) end end else - user, sid = fetch_user(sid, headers, db, logger) + user, sid = fetch_user(sid, headers, db) user_array = user.to_a user_array[4] = user_array[4].to_json # User preferences args = arg_array(user.to_a) @@ -313,7 +313,7 @@ def get_user(sid, headers, db, logger, refresh = true) return user, sid end -def fetch_user(sid, headers, db, logger) +def fetch_user(sid, headers, db) feed = YT_POOL.client &.get("/subscription_manager?disable_polymer=1", headers) feed = XML.parse_html(feed.body) @@ -326,7 +326,7 @@ def fetch_user(sid, headers, db, logger) end end - channels = get_batch_channels(channels, db, logger, false, false) + channels = get_batch_channels(channels, db, false, false) email = feed.xpath_node(%q(//a[@class="yt-masthead-picker-header yt-masthead-picker-active-account"])) if email From df9e7f284c95c151a006eedf6be2ead49987e778 Mon Sep 17 00:00:00 2001 From: saltycrys <73420320+saltycrys@users.noreply.github.com> Date: Mon, 4 Jan 2021 17:05:45 +0100 Subject: [PATCH 0279/2930] Adjust log verbosity The default log level has been changed from `debug` to `info`. The `debug` log level is now more verbose. `debug` now gives a general overview of what is happening (where implemented) while `trace` gives all available details. --- src/invidious/channels.cr | 1 + src/invidious/helpers/helpers.cr | 2 +- src/invidious/jobs/refresh_channels_job.cr | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/invidious/channels.cr b/src/invidious/channels.cr index 274e86f9..9986fe1b 100644 --- a/src/invidious/channels.cr +++ b/src/invidious/channels.cr @@ -203,6 +203,7 @@ def get_channel(id, db, refresh = true, pull_all_videos = true) end def fetch_channel(ucid, db, pull_all_videos = true, locale = nil) + LOGGER.debug("fetch_channel: #{ucid}") LOGGER.trace("fetch_channel: #{ucid} : pull_all_videos = #{pull_all_videos}, locale = #{locale}") LOGGER.trace("fetch_channel: #{ucid} : Downloading RSS feed") diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr index cf965dad..e41f8317 100644 --- a/src/invidious/helpers/helpers.cr +++ b/src/invidious/helpers/helpers.cr @@ -67,7 +67,7 @@ class Config 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::Debug # Default log level, valid YAML values are ints and strings, see src/invidious/helpers/logger.cr + 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 # Database configuration 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 diff --git a/src/invidious/jobs/refresh_channels_job.cr b/src/invidious/jobs/refresh_channels_job.cr index 0c6ef79e..3e94a56e 100644 --- a/src/invidious/jobs/refresh_channels_job.cr +++ b/src/invidious/jobs/refresh_channels_job.cr @@ -26,7 +26,7 @@ class Invidious::Jobs::RefreshChannelsJob < Invidious::Jobs::BaseJob end end - LOGGER.trace("RefreshChannelsJob: #{id} : Spawning fiber") + LOGGER.debug("RefreshChannelsJob: #{id} : Spawning fiber") active_fibers += 1 spawn do begin @@ -52,7 +52,7 @@ class Invidious::Jobs::RefreshChannelsJob < Invidious::Jobs::BaseJob end end ensure - LOGGER.trace("RefreshChannelsJob: #{id} fiber : Done") + LOGGER.debug("RefreshChannelsJob: #{id} fiber : Done") active_channel.send(true) end end From 4d512d908dfaf9b726602c77bcf555d36445f88f Mon Sep 17 00:00:00 2001 From: HackerNCoder Date: Thu, 7 Jan 2021 19:01:13 +0100 Subject: [PATCH 0280/2930] Remove some mentions of omarroth --- kubernetes/Chart.yaml | 4 ++-- kubernetes/values.yaml | 2 +- shard.yml | 1 + src/invidious/helpers/helpers.cr | 2 +- src/invidious/users.cr | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/kubernetes/Chart.yaml b/kubernetes/Chart.yaml index bb0838ad..9e4b793e 100644 --- a/kubernetes/Chart.yaml +++ b/kubernetes/Chart.yaml @@ -9,9 +9,9 @@ keywords: - video - privacy home: https://invidio.us/ -icon: https://raw.githubusercontent.com/omarroth/invidious/05988c1c49851b7d0094fca16aeaf6382a7f64ab/assets/favicon-32x32.png +icon: https://raw.githubusercontent.com/iv-org/invidious/05988c1c49851b7d0094fca16aeaf6382a7f64ab/assets/favicon-32x32.png sources: -- https://github.com/omarroth/invidious +- https://github.com/iv-org/invidious maintainers: - name: Leon Klingele email: mail@leonklingele.de diff --git a/kubernetes/values.yaml b/kubernetes/values.yaml index 4d037022..08def6e4 100644 --- a/kubernetes/values.yaml +++ b/kubernetes/values.yaml @@ -1,7 +1,7 @@ name: invidious image: - repository: omarroth/invidious + repository: iv-org/invidious tag: latest pullPolicy: Always diff --git a/shard.yml b/shard.yml index 2b59786e..e0fa1d25 100644 --- a/shard.yml +++ b/shard.yml @@ -3,6 +3,7 @@ version: 0.20.1 authors: - Omar Roth + - Invidous team targets: invidious: diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr index e41f8317..a4101ef1 100644 --- a/src/invidious/helpers/helpers.cr +++ b/src/invidious/helpers/helpers.cr @@ -95,7 +95,7 @@ class Config 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 admin_email : String = "omarroth@protonmail.com" # Email for bug reports + property admin_email : String = "FIXME" # Email for bug reports @[YAML::Field(converter: Preferences::StringToCookies)] property cookies : HTTP::Cookies = HTTP::Cookies.new # Saved cookies in "name1=value1; name2=value2..." format diff --git a/src/invidious/users.cr b/src/invidious/users.cr index 6bc2acd2..4b16affc 100644 --- a/src/invidious/users.cr +++ b/src/invidious/users.cr @@ -427,7 +427,7 @@ def generate_captcha(key, db) end def generate_text_captcha(key, db) - response = make_client(TEXTCAPTCHA_URL, &.get("/omarroth@protonmail.com.json").body) + response = make_client(TEXTCAPTCHA_URL, &.get("/FIXME.json").body) response = JSON.parse(response) tokens = response["a"].as_a.map do |answer| From b0b8ba7000e76e7d5632e9edfebf5c8c4aa44111 Mon Sep 17 00:00:00 2001 From: saltycrys <73420320+saltycrys@users.noreply.github.com> Date: Thu, 7 Jan 2021 20:15:26 +0100 Subject: [PATCH 0281/2930] Respect `use_pubsub_feeds` config Setting `use_pubsub_feeds: false` now properly disables it. --- src/invidious.cr | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/invidious.cr b/src/invidious.cr index 2b915350..800ea956 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -169,7 +169,6 @@ end Invidious::Jobs.register Invidious::Jobs::RefreshChannelsJob.new(PG_DB, config) Invidious::Jobs.register Invidious::Jobs::RefreshFeedsJob.new(PG_DB, config) -Invidious::Jobs.register Invidious::Jobs::SubscribeToFeedsJob.new(PG_DB, config, HMAC_KEY) DECRYPT_FUNCTION = DecryptFunction.new(CONFIG.decrypt_polling) if config.decrypt_polling @@ -180,6 +179,10 @@ if config.statistics_enabled Invidious::Jobs.register Invidious::Jobs::StatisticsRefreshJob.new(PG_DB, config, SOFTWARE) end +if (config.use_pubsub_feeds.is_a?(Bool) && config.use_pubsub_feeds.as(Bool)) || (config.use_pubsub_feeds.is_a?(Int32) && config.use_pubsub_feeds.as(Int32) > 0) + Invidious::Jobs.register Invidious::Jobs::SubscribeToFeedsJob.new(PG_DB, config, HMAC_KEY) +end + if config.popular_enabled Invidious::Jobs.register Invidious::Jobs::PullPopularVideosJob.new(PG_DB) end From 606dd11b4f9fb6fc2aaca85e18e6a2d750d5ad6c Mon Sep 17 00:00:00 2001 From: HackerNCoder Date: Thu, 7 Jan 2021 21:09:24 +0100 Subject: [PATCH 0282/2930] Remove admin_email. Use repos url for captcha ID and reddit header. Add note about not updating changelog --- CHANGELOG.md | 2 ++ src/invidious/comments.cr | 2 +- src/invidious/helpers/helpers.cr | 1 - src/invidious/users.cr | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 314a134f..8aa416ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,5 @@ +# Note: This is no longer updated and links to omarroths repo, which doesn't exist anymore. + # 0.20.0 (2019-011-06) # Version 0.20.0: Custom Playlists diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr index 8849c87f..0ac99ba5 100644 --- a/src/invidious/comments.cr +++ b/src/invidious/comments.cr @@ -242,7 +242,7 @@ end def fetch_reddit_comments(id, sort_by = "confidence") client = make_client(REDDIT_URL) - headers = HTTP::Headers{"User-Agent" => "web:invidious:v#{CURRENT_VERSION} (by /u/omarroth)"} + headers = HTTP::Headers{"User-Agent" => "web:invidious:v#{CURRENT_VERSION} (by github.com/iv-org/invidious)"} # TODO: Use something like #479 for a static list of instances to use here query = "(url:3D#{id}%20OR%20url:#{id})%20(site:invidio.us%20OR%20site:youtube.com%20OR%20site:youtu.be)" diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr index a4101ef1..1f56ec92 100644 --- a/src/invidious/helpers/helpers.cr +++ b/src/invidious/helpers/helpers.cr @@ -95,7 +95,6 @@ class Config 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 admin_email : String = "FIXME" # Email for bug reports @[YAML::Field(converter: Preferences::StringToCookies)] property cookies : HTTP::Cookies = HTTP::Cookies.new # Saved cookies in "name1=value1; name2=value2..." format diff --git a/src/invidious/users.cr b/src/invidious/users.cr index 4b16affc..153e3b6a 100644 --- a/src/invidious/users.cr +++ b/src/invidious/users.cr @@ -427,7 +427,7 @@ def generate_captcha(key, db) end def generate_text_captcha(key, db) - response = make_client(TEXTCAPTCHA_URL, &.get("/FIXME.json").body) + response = make_client(TEXTCAPTCHA_URL, &.get("/github.com/iv.org/invidious.json").body) response = JSON.parse(response) tokens = response["a"].as_a.map do |answer| From c81ca187f8b31a04b1027c63fef99711cbda0556 Mon Sep 17 00:00:00 2001 From: saltycrys <73420320+saltycrys@users.noreply.github.com> Date: Sat, 9 Jan 2021 20:40:01 +0100 Subject: [PATCH 0283/2930] Fix downloads The `itag` is now converted to a number, matching the `itag` of `Video.adaptive_fmts` and `Video.fmt_stream`. --- src/invidious.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious.cr b/src/invidious.cr index 800ea956..5235d826 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -3372,7 +3372,7 @@ get "/latest_version" do |env| env.redirect "/api/v1/captions/#{id}?label=#{label}&title=#{title}" next else - itag = download_widget["itag"].as_s + itag = download_widget["itag"].as_s.to_i local = "true" end end From 5a08dfa72f26a250b09c643c47cd4abc18e3843c Mon Sep 17 00:00:00 2001 From: Andrew Zhao Date: Sun, 10 Jan 2021 17:05:08 -0500 Subject: [PATCH 0284/2930] fix incorrect use of setinterval in js --- assets/js/player.js | 4 ++-- assets/js/watch.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/js/player.js b/assets/js/player.js index fcba43d8..932de581 100644 --- a/assets/js/player.js +++ b/assets/js/player.js @@ -69,7 +69,7 @@ if (location.pathname.startsWith('/embed/')) { player.on('error', function (event) { if (player.error().code === 2 || player.error().code === 4) { - setInterval(setTimeout(function (event) { + setTimeout(function (event) { console.log('An error occured in the player, reloading...'); var currentTime = player.currentTime(); @@ -88,7 +88,7 @@ player.on('error', function (event) { if (!paused) { player.play(); } - }, 5000), 5000); + }, 5000); } }); diff --git a/assets/js/watch.js b/assets/js/watch.js index 05530f3d..eb493bf3 100644 --- a/assets/js/watch.js +++ b/assets/js/watch.js @@ -272,7 +272,7 @@ function get_reddit_comments(retries) { xhr.onerror = function () { console.log('Pulling comments failed... ' + retries + '/5'); - setInterval(function () { get_reddit_comments(retries - 1) }, 1000); + setTimeout(function () { get_reddit_comments(retries - 1) }, 1000); } xhr.ontimeout = function () { @@ -346,7 +346,7 @@ function get_youtube_comments(retries) { comments.innerHTML = '

'; console.log('Pulling comments failed... ' + retries + '/5'); - setInterval(function () { get_youtube_comments(retries - 1) }, 1000); + setTimeout(function () { get_youtube_comments(retries - 1) }, 1000); } xhr.ontimeout = function () { From b7fe1db89a7b33ab2f9240e69acb3dfcd5210bd5 Mon Sep 17 00:00:00 2001 From: Andrew Zhao Date: Sun, 10 Jan 2021 18:00:45 -0500 Subject: [PATCH 0285/2930] save host when using dash manifest --- src/invidious.cr | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/invidious.cr b/src/invidious.cr index 5235d826..deb24ac3 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -3190,7 +3190,8 @@ get "/api/manifest/dash/id/:id" do |env| url = url.rchop("") if local - url = URI.parse(url).full_path + uri = URI.parse(url) + url = "#{uri.full_path}host/#{uri.host}/" end "#{url}" From 8584654f11b6596a4b0cd0219b699eb4f85df5c5 Mon Sep 17 00:00:00 2001 From: Perflyst Date: Sat, 16 Jan 2021 22:18:22 +0100 Subject: [PATCH 0286/2930] Add container CI release --- .github/workflows/release.yml | 37 +++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..77877080 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,37 @@ +name: Build and release container + +on: + push: + branches: + - "master" + +jobs: + release: + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Login to registry + uses: docker/login-action@v1 + with: + registry: quay.io + username: ${{ secrets.QUAY_USERNAME }} + password: ${{ secrets.QUAY_PASSWORD }} + + - name: Build and push + uses: docker/build-push-action@v2 + with: + push: true + tags: quay.io/invidious/invidious:latest + + - name: Image digest + run: echo ${{ steps.docker_build.outputs.digest }} From ffa9e5dfab7a1d7ea84d88007107f9f40295c50a Mon Sep 17 00:00:00 2001 From: Andre Borie Date: Sun, 17 Jan 2021 01:44:31 +0000 Subject: [PATCH 0287/2930] Make migrations (mostly) idempotent. --- config/sql/annotations.sql | 4 ++-- config/sql/channel_videos.sql | 6 +++--- config/sql/channels.sql | 6 +++--- config/sql/nonces.sql | 6 +++--- config/sql/playlist_videos.sql | 4 ++-- config/sql/playlists.sql | 4 ++-- config/sql/session_ids.sql | 6 +++--- config/sql/users.sql | 6 +++--- config/sql/videos.sql | 6 +++--- docker/init-invidious-db.sh | 18 +++++++++--------- 10 files changed, 33 insertions(+), 33 deletions(-) diff --git a/config/sql/annotations.sql b/config/sql/annotations.sql index 4ea077e7..3705829d 100644 --- a/config/sql/annotations.sql +++ b/config/sql/annotations.sql @@ -2,11 +2,11 @@ -- DROP TABLE public.annotations; -CREATE TABLE public.annotations +CREATE TABLE IF NOT EXISTS public.annotations ( id text NOT NULL, annotations xml, CONSTRAINT annotations_id_key UNIQUE (id) ); -GRANT ALL ON TABLE public.annotations TO kemal; +GRANT ALL ON TABLE public.annotations TO current_user; diff --git a/config/sql/channel_videos.sql b/config/sql/channel_videos.sql index cec57cd4..cd4e0ffd 100644 --- a/config/sql/channel_videos.sql +++ b/config/sql/channel_videos.sql @@ -2,7 +2,7 @@ -- DROP TABLE public.channel_videos; -CREATE TABLE public.channel_videos +CREATE TABLE IF NOT EXISTS public.channel_videos ( id text NOT NULL, title text, @@ -17,13 +17,13 @@ CREATE TABLE public.channel_videos CONSTRAINT channel_videos_id_key UNIQUE (id) ); -GRANT ALL ON TABLE public.channel_videos TO kemal; +GRANT ALL ON TABLE public.channel_videos TO current_user; -- Index: public.channel_videos_ucid_idx -- DROP INDEX public.channel_videos_ucid_idx; -CREATE INDEX channel_videos_ucid_idx +CREATE INDEX IF NOT EXISTS channel_videos_ucid_idx ON public.channel_videos USING btree (ucid COLLATE pg_catalog."default"); diff --git a/config/sql/channels.sql b/config/sql/channels.sql index b5a29b8f..55772da6 100644 --- a/config/sql/channels.sql +++ b/config/sql/channels.sql @@ -2,7 +2,7 @@ -- DROP TABLE public.channels; -CREATE TABLE public.channels +CREATE TABLE IF NOT EXISTS public.channels ( id text NOT NULL, author text, @@ -12,13 +12,13 @@ CREATE TABLE public.channels CONSTRAINT channels_id_key UNIQUE (id) ); -GRANT ALL ON TABLE public.channels TO kemal; +GRANT ALL ON TABLE public.channels TO current_user; -- Index: public.channels_id_idx -- DROP INDEX public.channels_id_idx; -CREATE INDEX channels_id_idx +CREATE INDEX IF NOT EXISTS channels_id_idx ON public.channels USING btree (id COLLATE pg_catalog."default"); diff --git a/config/sql/nonces.sql b/config/sql/nonces.sql index 7b8ce9f2..644ac32a 100644 --- a/config/sql/nonces.sql +++ b/config/sql/nonces.sql @@ -2,20 +2,20 @@ -- DROP TABLE public.nonces; -CREATE TABLE public.nonces +CREATE TABLE IF NOT EXISTS public.nonces ( nonce text, expire timestamp with time zone, CONSTRAINT nonces_id_key UNIQUE (nonce) ); -GRANT ALL ON TABLE public.nonces TO kemal; +GRANT ALL ON TABLE public.nonces TO current_user; -- Index: public.nonces_nonce_idx -- DROP INDEX public.nonces_nonce_idx; -CREATE INDEX nonces_nonce_idx +CREATE INDEX IF NOT EXISTS nonces_nonce_idx ON public.nonces USING btree (nonce COLLATE pg_catalog."default"); diff --git a/config/sql/playlist_videos.sql b/config/sql/playlist_videos.sql index b2b8d5c4..eedccbad 100644 --- a/config/sql/playlist_videos.sql +++ b/config/sql/playlist_videos.sql @@ -2,7 +2,7 @@ -- DROP TABLE public.playlist_videos; -CREATE TABLE playlist_videos +CREATE TABLE IF NOT EXISTS playlist_videos ( title text, id text, @@ -16,4 +16,4 @@ CREATE TABLE playlist_videos PRIMARY KEY (index,plid) ); -GRANT ALL ON TABLE public.playlist_videos TO kemal; +GRANT ALL ON TABLE public.playlist_videos TO current_user; diff --git a/config/sql/playlists.sql b/config/sql/playlists.sql index 468496cb..83efce48 100644 --- a/config/sql/playlists.sql +++ b/config/sql/playlists.sql @@ -13,7 +13,7 @@ CREATE TYPE public.privacy AS ENUM -- DROP TABLE public.playlists; -CREATE TABLE public.playlists +CREATE TABLE IF NOT EXISTS public.playlists ( title text, id text primary key, @@ -26,4 +26,4 @@ CREATE TABLE public.playlists index int8[] ); -GRANT ALL ON public.playlists TO kemal; +GRANT ALL ON public.playlists TO current_user; diff --git a/config/sql/session_ids.sql b/config/sql/session_ids.sql index afbabb67..c493769a 100644 --- a/config/sql/session_ids.sql +++ b/config/sql/session_ids.sql @@ -2,7 +2,7 @@ -- DROP TABLE public.session_ids; -CREATE TABLE public.session_ids +CREATE TABLE IF NOT EXISTS public.session_ids ( id text NOT NULL, email text, @@ -10,13 +10,13 @@ CREATE TABLE public.session_ids CONSTRAINT session_ids_pkey PRIMARY KEY (id) ); -GRANT ALL ON TABLE public.session_ids TO kemal; +GRANT ALL ON TABLE public.session_ids TO current_user; -- Index: public.session_ids_id_idx -- DROP INDEX public.session_ids_id_idx; -CREATE INDEX session_ids_id_idx +CREATE INDEX IF NOT EXISTS session_ids_id_idx ON public.session_ids USING btree (id COLLATE pg_catalog."default"); diff --git a/config/sql/users.sql b/config/sql/users.sql index 0f2cdba2..ad002ec2 100644 --- a/config/sql/users.sql +++ b/config/sql/users.sql @@ -2,7 +2,7 @@ -- DROP TABLE public.users; -CREATE TABLE public.users +CREATE TABLE IF NOT EXISTS public.users ( updated timestamp with time zone, notifications text[], @@ -16,13 +16,13 @@ CREATE TABLE public.users CONSTRAINT users_email_key UNIQUE (email) ); -GRANT ALL ON TABLE public.users TO kemal; +GRANT ALL ON TABLE public.users TO current_user; -- Index: public.email_unique_idx -- DROP INDEX public.email_unique_idx; -CREATE UNIQUE INDEX email_unique_idx +CREATE UNIQUE INDEX IF NOT EXISTS email_unique_idx ON public.users USING btree (lower(email) COLLATE pg_catalog."default"); diff --git a/config/sql/videos.sql b/config/sql/videos.sql index 8def2f83..7040703c 100644 --- a/config/sql/videos.sql +++ b/config/sql/videos.sql @@ -2,7 +2,7 @@ -- DROP TABLE public.videos; -CREATE TABLE public.videos +CREATE TABLE IF NOT EXISTS public.videos ( id text NOT NULL, info text, @@ -10,13 +10,13 @@ CREATE TABLE public.videos CONSTRAINT videos_pkey PRIMARY KEY (id) ); -GRANT ALL ON TABLE public.videos TO kemal; +GRANT ALL ON TABLE public.videos TO current_user; -- Index: public.id_idx -- DROP INDEX public.id_idx; -CREATE UNIQUE INDEX id_idx +CREATE UNIQUE INDEX IF NOT EXISTS id_idx ON public.videos USING btree (id COLLATE pg_catalog."default"); diff --git a/docker/init-invidious-db.sh b/docker/init-invidious-db.sh index 3808e673..cc0e1c3f 100755 --- a/docker/init-invidious-db.sh +++ b/docker/init-invidious-db.sh @@ -5,12 +5,12 @@ psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-E CREATE USER postgres; EOSQL -psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/channels.sql -psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/videos.sql -psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/channel_videos.sql -psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/users.sql -psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/session_ids.sql -psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/nonces.sql -psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/annotations.sql -psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/playlists.sql -psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/playlist_videos.sql +psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/channels.sql +psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/videos.sql +psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/channel_videos.sql +psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/users.sql +psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/session_ids.sql +psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/nonces.sql +psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/annotations.sql +psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/playlists.sql +psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/playlist_videos.sql From 3046350cb2efcac907d6ce9fa4b3a08bda25c128 Mon Sep 17 00:00:00 2001 From: Andre Borie Date: Sun, 17 Jan 2021 01:43:36 +0000 Subject: [PATCH 0288/2930] Fix DASH playback bug. --- assets/js/player.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/assets/js/player.js b/assets/js/player.js index 5d045391..04326631 100644 --- a/assets/js/player.js +++ b/assets/js/player.js @@ -19,6 +19,11 @@ var options = { 'playbackRateMenuButton', 'fullscreenToggle' ] + }, + html5: { + hls: { + overrideNative: true + } } } From 15ba3325d9eee4e8b30828bf6d239f4999a36e0b Mon Sep 17 00:00:00 2001 From: Andrew Zhao Date: Sat, 2 Jan 2021 19:35:31 -0500 Subject: [PATCH 0289/2930] add ui for searching --- assets/css/default.css | 3 + assets/js/search.js | 13 +++++ locales/en-US.json | 27 +++++++++ src/invidious/routes/playlists.cr | 2 +- src/invidious/routes/search.cr | 8 ++- src/invidious/search.cr | 2 +- src/invidious/views/search.ecr | 97 +++++++++++++++++++++++++++++++ 7 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 assets/js/search.js diff --git a/assets/css/default.css b/assets/css/default.css index e403e606..793295d9 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -628,3 +628,6 @@ body.dark-theme { } } +#filters { + display: none; +} \ No newline at end of file diff --git a/assets/js/search.js b/assets/js/search.js new file mode 100644 index 00000000..36edd053 --- /dev/null +++ b/assets/js/search.js @@ -0,0 +1,13 @@ +function toggle_comments(event) { + var target = event.target; + var body = document.getElementById('filters'); + if (body.style.display === 'flex') { + target.innerHTML = '[ + ]'; + body.style.display = 'none'; + } else { + target.innerHTML = '[ - ]'; + body.style.display = 'flex'; + } +} + +document.getElementById('togglefilters').onclick = toggle_comments; \ No newline at end of file diff --git a/locales/en-US.json b/locales/en-US.json index acd2b667..66e71bb6 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -383,5 +383,32 @@ "Videos": "Videos", "Playlists": "Playlists", "Community": "Community", + "relevance": "Relevance", + "rating": "Rating", + "date": "Upload date", + "views": "View count", + "content_type": "Type", + "duration": "Duration", + "features": "Features", + "sort": "Sort By", + "hour": "Last Hour", + "today": "Today", + "week": "This week", + "month": "This month", + "year": "This year", + "video": "Video", + "channel": "Channel", + "playlist": "Playlist", + "movie": "Movie", + "show": "Show", + "hd": "HD", + "subtitles": "Subtitles/CC", + "creative_commons": "Creative Commons", + "3d": "3D", + "live": "Live", + "4k": "4K", + "location": "Location", + "hdr": "HDR", + "filter": "Filter", "Current version: ": "Current version: " } \ No newline at end of file diff --git a/src/invidious/routes/playlists.cr b/src/invidious/routes/playlists.cr index 6c899054..c5023c08 100644 --- a/src/invidious/routes/playlists.cr +++ b/src/invidious/routes/playlists.cr @@ -267,7 +267,7 @@ class Invidious::Routes::Playlists < Invidious::Routes::BaseRoute query = env.params.query["q"]? if query begin - search_query, count, items = process_search_query(query, page, user, region: nil) + search_query, count, items, operators = process_search_query(query, page, user, region: nil) videos = items.select { |item| item.is_a? SearchVideo }.map { |item| item.as(SearchVideo) } rescue ex videos = [] of SearchVideo diff --git a/src/invidious/routes/search.cr b/src/invidious/routes/search.cr index 48446161..a993a17a 100644 --- a/src/invidious/routes/search.cr +++ b/src/invidious/routes/search.cr @@ -48,11 +48,17 @@ class Invidious::Routes::Search < Invidious::Routes::BaseRoute user = env.get? "user" begin - search_query, count, videos = process_search_query(query, page, user, region: nil) + search_query, count, videos, operators = process_search_query(query, page, user, region: nil) rescue ex return error_template(500, ex) end + operator_hash = {} of String => String + operators.each do |operator| + key, value = operator.downcase.split(":") + operator_hash[key] = value + end + env.set "search", query templated "search" end diff --git a/src/invidious/search.cr b/src/invidious/search.cr index 85fd024a..1c4bc74e 100644 --- a/src/invidious/search.cr +++ b/src/invidious/search.cr @@ -445,5 +445,5 @@ def process_search_query(query, page, user, region) count, items = search(search_query, page, search_params, region).as(Tuple) end - {search_query, count, items} + {search_query, count, items, operators} end diff --git a/src/invidious/views/search.ecr b/src/invidious/views/search.ecr index bc13b7ea..3fa9242b 100644 --- a/src/invidious/views/search.ecr +++ b/src/invidious/views/search.ecr @@ -2,6 +2,102 @@ <%= search_query.not_nil!.size > 30 ? HTML.escape(query.not_nil![0,30].rstrip(".") + "...") : HTML.escape(query.not_nil!) %> - Invidious <% end %> +

+ [ + ] + <%= translate(locale, "filter") %> +

+ + +
+
+ <%= translate(locale, "date") %> +
+ <% ["hour", "today", "week", "month", "year"].each do |date| %> +
+ <% if operator_hash.fetch("date", "all") == date %> + <%= translate(locale, date) %> + <% else %> + &page=<%= page %>"> + <%= translate(locale, date) %> + + <% end %> +
+ <% end %> +
+
+ <%= translate(locale, "content_type") %> +
+ <% ["video", "channel", "playlist", "movie", "show"].each do |content_type| %> +
+ <% if operator_hash.fetch("content_type", "all") == content_type %> + <%= translate(locale, content_type) %> + <% else %> + &page=<%= page %>"> + <%= translate(locale, content_type) %> + + <% end %> +
+ <% end %> +
+
+ <%= translate(locale, "duration") %> +
+ <% ["short", "long"].each do |duration| %> +
+ <% if operator_hash.fetch("duration", "all") == duration %> + <%= translate(locale, duration) %> + <% else %> + &page=<%= page %>"> + <%= translate(locale, duration) %> + + <% end %> +
+ <% end %> +
+
+ <%= translate(locale, "features") %> +
+ <% ["hd", "subtitles", "creative_commons", "3d", "live", "purchased", "4k", "360", "location", "hdr"].each do |feature| %> +
+ <% if operator_hash.fetch("features", "all").includes?(feature) %> + <%= translate(locale, feature) %> + <% elsif operator_hash.has_key?("features") %> + &page=<%= page %>"> + <%= translate(locale, feature) %> + + <% else %> + &page=<%= page %>"> + <%= translate(locale, feature) %> + + <% end %> +
+ <% end %> +
+
+ <%= translate(locale, "sort") %> +
+ <% ["relevance", "rating", "date", "views"].each do |sort| %> +
+ <% if operator_hash.fetch("sort", "relevance") == sort %> + <%= translate(locale, sort) %> + <% else %> + &page=<%= page %>"> + <%= translate(locale, sort) %> + + <% end %> +
+ <% end %> +
+
+ +
+
<% if page > 1 %> @@ -45,3 +141,4 @@ <% end %>
+ \ No newline at end of file From d4ddd7204d7f260574bb6c572e90bcdc99e7dbd0 Mon Sep 17 00:00:00 2001 From: Perflyst Date: Wed, 20 Jan 2021 12:44:01 +0100 Subject: [PATCH 0290/2930] Rename release.yml to container-release.yml --- .github/workflows/{release.yml => container-release.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{release.yml => container-release.yml} (100%) diff --git a/.github/workflows/release.yml b/.github/workflows/container-release.yml similarity index 100% rename from .github/workflows/release.yml rename to .github/workflows/container-release.yml From a1aa40f500a9a70f58d1a7d43ef17b37f9cce1df Mon Sep 17 00:00:00 2001 From: Perflyst Date: Thu, 21 Jan 2021 07:45:43 +0100 Subject: [PATCH 0291/2930] Fix container build --- .github/workflows/container-release.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/container-release.yml b/.github/workflows/container-release.yml index 77877080..b21123d2 100644 --- a/.github/workflows/container-release.yml +++ b/.github/workflows/container-release.yml @@ -30,6 +30,8 @@ jobs: - name: Build and push uses: docker/build-push-action@v2 with: + context: . + file: docker/Dockerfile push: true tags: quay.io/invidious/invidious:latest From a00453e1510e6d00d7f109080b37352647a3a83e Mon Sep 17 00:00:00 2001 From: Perflyst Date: Thu, 21 Jan 2021 23:51:54 +0100 Subject: [PATCH 0292/2930] Build latest only on master, add commit sha tag Close #1688 --- .github/workflows/container-release.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/container-release.yml b/.github/workflows/container-release.yml index b21123d2..3d61928f 100644 --- a/.github/workflows/container-release.yml +++ b/.github/workflows/container-release.yml @@ -4,6 +4,8 @@ on: push: branches: - "master" + pull_request: + branches: "*" jobs: release: @@ -27,7 +29,8 @@ jobs: username: ${{ secrets.QUAY_USERNAME }} password: ${{ secrets.QUAY_PASSWORD }} - - name: Build and push + - name: Build and push latest tag + if: github.ref == 'refs/heads/master' uses: docker/build-push-action@v2 with: context: . @@ -35,5 +38,13 @@ jobs: push: true tags: quay.io/invidious/invidious:latest + - name: Build and push commit sha tag + uses: docker/build-push-action@v2 + with: + context: . + file: docker/Dockerfile + push: true + tags: quay.io/invidious/invidious:${{ github.sha }} + - name: Image digest run: echo ${{ steps.docker_build.outputs.digest }} From 5ce0b1c18ee3ea722f2f1b1919509bf16c6ba1f6 Mon Sep 17 00:00:00 2001 From: Perflyst Date: Thu, 21 Jan 2021 23:54:31 +0100 Subject: [PATCH 0293/2930] Remove image digest output Does not work with two images at the same time --- .github/workflows/container-release.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/container-release.yml b/.github/workflows/container-release.yml index 3d61928f..c8b2e56a 100644 --- a/.github/workflows/container-release.yml +++ b/.github/workflows/container-release.yml @@ -45,6 +45,3 @@ jobs: file: docker/Dockerfile push: true tags: quay.io/invidious/invidious:${{ github.sha }} - - - name: Image digest - run: echo ${{ steps.docker_build.outputs.digest }} From 380b64071eb79ad92ab5d608c1b0b69ffca5d7d6 Mon Sep 17 00:00:00 2001 From: Perflyst Date: Fri, 22 Jan 2021 00:03:09 +0100 Subject: [PATCH 0294/2930] Expire sha images after 6 weeks --- .github/workflows/container-release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/container-release.yml b/.github/workflows/container-release.yml index c8b2e56a..b1a04f26 100644 --- a/.github/workflows/container-release.yml +++ b/.github/workflows/container-release.yml @@ -43,5 +43,6 @@ jobs: with: context: . file: docker/Dockerfile + labels: quay.expires-after=6w push: true tags: quay.io/invidious/invidious:${{ github.sha }} From 40a257982164c70cb12d421f835220abd41ea3c9 Mon Sep 17 00:00:00 2001 From: FireMasterK <20838718+FireMasterK@users.noreply.github.com> Date: Fri, 22 Jan 2021 09:20:17 +0530 Subject: [PATCH 0295/2930] different steps depending on event. --- .github/workflows/container-release.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/container-release.yml b/.github/workflows/container-release.yml index b1a04f26..1859f082 100644 --- a/.github/workflows/container-release.yml +++ b/.github/workflows/container-release.yml @@ -9,7 +9,6 @@ on: jobs: release: - runs-on: ubuntu-latest steps: @@ -29,16 +28,18 @@ jobs: username: ${{ secrets.QUAY_USERNAME }} password: ${{ secrets.QUAY_PASSWORD }} - - name: Build and push latest tag + - name: Build and push for Push Event if: github.ref == 'refs/heads/master' uses: docker/build-push-action@v2 with: context: . file: docker/Dockerfile + labels: quay.expires-after=12w push: true - tags: quay.io/invidious/invidious:latest + tags: quay.io/invidious/invidious:${{ github.sha }},quay.io/invidious/invidious:latest - - name: Build and push commit sha tag + - name: Build and push for Pull Request + if: github.ref != 'refs/heads/master' uses: docker/build-push-action@v2 with: context: . From c86e9dfc8a4f2dddc511f08f19fdeba97ab11143 Mon Sep 17 00:00:00 2001 From: FireMasterK <20838718+FireMasterK@users.noreply.github.com> Date: Fri, 22 Jan 2021 16:54:49 +0530 Subject: [PATCH 0296/2930] build image daily at 0:00 GMT --- .github/workflows/container-release.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/container-release.yml b/.github/workflows/container-release.yml index 1859f082..137308d0 100644 --- a/.github/workflows/container-release.yml +++ b/.github/workflows/container-release.yml @@ -6,6 +6,8 @@ on: - "master" pull_request: branches: "*" + schedule: + - cron: 0 0 * * * jobs: release: From 799f97e847fe92ce2463847b821a499ca54c8e31 Mon Sep 17 00:00:00 2001 From: TheFrenchGhosty Date: Sat, 23 Jan 2021 18:07:55 +0100 Subject: [PATCH 0297/2930] Make invidious use all the translation files --- src/invidious.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index deb24ac3..10c23dac 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -84,9 +84,9 @@ LOCALES = { "en-US" => load_locale("en-US"), "eo" => load_locale("eo"), "es" => load_locale("es"), - "eu" => load_locale("eu"), + "fa" => load_locale("fa"), "fr" => load_locale("fr"), - "hu" => load_locale("hu-HU"), + "hr" => load_locale("hr"), "is" => load_locale("is"), "it" => load_locale("it"), "ja" => load_locale("ja"), From 1996e6afaad350e5205135919461b96fe04e12ac Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Sat, 23 Jan 2021 18:10:51 +0100 Subject: [PATCH 0298/2930] Translations update from Weblate (#1696) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update German translation * Add Bengali (Bangladesh) translation * Update Bengali (Bangladesh) translation * Update Portuguese (Portugal) translation * Update Icelandic translation * Update Bengali (Bangladesh) translation * Update Bengali (Bangladesh) translation * Add Catalan translation * Update Norwegian Bokmål translation * Add French (Canada) translation * Update German translation * Update Polish translation * Update Dutch translation * Update French translation * Update Italian translation * Update Greek translation * Update French (Canada) translation * Update Turkish translation * Update Norwegian Bokmål translation * Update Esperanto translation * Update Esperanto translation * Add Slovak translation * Update Slovak translation * Update Serbian (cyrillic) translation * Update Slovak translation * Update Esperanto translation * Add Persian translation * Update Persian translation * Add Kannada translation * Update Kannada translation * Update Bengali (Bangladesh) translation * Update Spanish translation * Update Portuguese (Brazil) translation * Update Chinese (Traditional) translation * Update Swedish translation * Update Portuguese (Portugal) translation * Add Finnish translation * Update Dutch translation * Update Finnish translation * Add Indonesian translation * Add Portuguese translation * Update Portuguese (Portugal) translation * Update Indonesian translation * Update Portuguese translation * Update Polish translation * Update Polish translation * Update Portuguese (Brazil) translation * Update Chinese (Simplified) translation * Add Croatian translation * Update Croatian translation * Update Norwegian Bokmål translation * Update Swedish translation * Update Croatian translation * Update Japanese translation * Update Indonesian translation * Add Danish translation * Update Bengali (Bangladesh) translation * Update Danish translation * Update Chinese (Simplified) translation * Update French (Canada) translation * Add Malayalam translation * Update Swedish translation * Update Greek translation * Update Greek translation * Update Portuguese (Brazil) translation * Delete Catalan translation * Delete Malayalam translation * Delete Kannada translation * Delete French (Canada) translation * Delete Portuguese translation Co-authored-by: Jeannette L Co-authored-by: Oymate Co-authored-by: Paulo Marinho Co-authored-by: recette-lemon <854qskawygnrtcdo@protonmail.com> Co-authored-by: Adolfo Jayme Barrientos Co-authored-by: Allan Nordhøy Co-authored-by: Oğuz Ersen Co-authored-by: Attila Farkas Co-authored-by: bongo bongo Co-authored-by: nathgit Co-authored-by: Kevin Scruff Co-authored-by: Yogesh Co-authored-by: The Cats Co-authored-by: FeiYang Co-authored-by: Luna Jernberg Co-authored-by: ssantos Co-authored-by: Unihuppio Co-authored-by: Joachim Opdenakker Co-authored-by: ziasukmana Co-authored-by: Atrate Co-authored-by: Karol Kosek Co-authored-by: André Marcelo Alvarenga Co-authored-by: Eric Co-authored-by: Milo Ivir Co-authored-by: Petter Reinholdtsen Co-authored-by: Y. Sakamoto Co-authored-by: Nimityx Co-authored-by: HackerNCoder Co-authored-by: vachan-maker Co-authored-by: fresh Co-authored-by: TheFrenchGhosty --- locales/bn_BD.json | 353 +++++++++++++++++++++++++++++++++++++++ locales/da.json | 387 +++++++++++++++++++++++++++++++++++++++++++ locales/de.json | 24 +-- locales/el.json | 148 +++++++---------- locales/eo.json | 14 +- locales/es.json | 6 +- locales/fa.json | 353 +++++++++++++++++++++++++++++++++++++++ locales/fi.json | 353 +++++++++++++++++++++++++++++++++++++++ locales/fr.json | 44 ++--- locales/hr.json | 387 +++++++++++++++++++++++++++++++++++++++++++ locales/id.json | 387 +++++++++++++++++++++++++++++++++++++++++++ locales/is.json | 66 ++++---- locales/it.json | 102 ++++-------- locales/ja.json | 6 +- locales/nb-NO.json | 144 ++++++++-------- locales/nl.json | 20 +-- locales/pl.json | 6 +- locales/pt-BR.json | 136 +++++++-------- locales/pt-PT.json | 124 +++++--------- locales/sk.json | 353 +++++++++++++++++++++++++++++++++++++++ locales/sr_Cyrl.json | 206 +++++++++++------------ locales/sv-SE.json | 216 ++++++++++++------------ locales/tr.json | 4 +- locales/zh-CN.json | 92 +++++----- locales/zh-TW.json | 92 ++++------ 25 files changed, 3236 insertions(+), 787 deletions(-) create mode 100644 locales/bn_BD.json create mode 100644 locales/da.json create mode 100644 locales/fa.json create mode 100644 locales/fi.json create mode 100644 locales/hr.json create mode 100644 locales/id.json create mode 100644 locales/sk.json diff --git a/locales/bn_BD.json b/locales/bn_BD.json new file mode 100644 index 00000000..8356424f --- /dev/null +++ b/locales/bn_BD.json @@ -0,0 +1,353 @@ +{ + "`x` subscribers.([^.,0-9]|^)1([^.,0-9]|$)": "`x` সাবস্ক্রাইবার।([^.,0-9]|^)1([^.,0-9]|$)", + "`x` subscribers.": "`x` সাবস্ক্রাইবার।", + "`x` videos.([^.,0-9]|^)1([^.,0-9]|$)": "`x` ভিডিও।([^.,0-9]|^)1([^.,0-9]|$)", + "`x` videos.": "`x` ভিডিও।", + "`x` playlists.([^.,0-9]|^)1([^.,0-9]|$)": "`x` প্লেলিস্ট।[^.,0-9]|^)1([^.,0-9]|$)", + "`x` playlists.": "`x` প্লেলিস্ট।", + "LIVE": "লাইভ", + "Shared `x` ago": "`x` আগে শেয়ার করা হয়েছে", + "Unsubscribe": "আনসাবস্ক্রাইব", + "Subscribe": "সাবস্ক্রাইব", + "View channel on YouTube": "ইউটিউবে চ্যানেল দেখুন", + "View playlist on YouTube": "ইউটিউবে প্লেলিস্ট দেখুন", + "newest": "সর্ব-নতুন", + "oldest": "পুরানতম", + "popular": "জনপ্রিয়", + "last": "শেষটা", + "Next page": "পরের পৃষ্ঠা", + "Previous page": "আগের পৃষ্ঠা", + "Clear watch history?": "দেখার ইতিহাস সাফ করবেন?", + "New password": "নতুন পাসওয়ার্ড", + "New passwords must match": "নতুন পাসওয়ার্ড অবশ্যই মিলতে হবে", + "Cannot change password for Google accounts": "গুগল অ্যাকাউন্টগুলোর জন্য পাসওয়ার্ড পরিবর্তন করা যায় না", + "Authorize token?": "টোকেন অনুমোদন করবেন?", + "Authorize token for `x`?": "`x` -এর জন্য টোকেন অনুমোদন?", + "Yes": "হ্যাঁ", + "No": "না", + "Import and Export Data": "তথ্য আমদানি ও রপ্তানি", + "Import": "আমদানি", + "Import Invidious data": "ইনভিডিয়াস তথ্য আমদানি", + "Import YouTube subscriptions": "ইউটিউব সাবস্ক্রিপশন আনুন", + "Import FreeTube subscriptions (.db)": "ফ্রিটিউব সাবস্ক্রিপশন (.db) আনুন", + "Import NewPipe subscriptions (.json)": "নতুন পাইপ সাবস্ক্রিপশন আনুন (.json)", + "Import NewPipe data (.zip)": "নিউপাইপ তথ্য আনুন (.zip)", + "Export": "তথ্য বের করুন", + "Export subscriptions as OPML": "সাবস্ক্রিপশন OPML হিসাবে আনুন", + "Export subscriptions as OPML (for NewPipe & FreeTube)": "OPML-এ সাবস্ক্রিপশন বের করুন(নিউ পাইপ এবং ফ্রিউটিউব এর জন্য)", + "Export data as JSON": "JSON হিসাবে তথ্য বের করুন", + "Delete account?": "অ্যাকাউন্ট মুছে ফেলবেন?", + "History": "ইতিহাস", + "An alternative front-end to YouTube": "ইউটিউবের একটি বিকল্পস্বরূপ সম্মুখ-প্রান্ত", + "JavaScript license information": "জাভাস্ক্রিপ্ট লাইসেন্সের তথ্য", + "source": "সূত্র", + "Log in": "লগ ইন", + "Log in/register": "লগ ইন/রেজিস্টার", + "Log in with Google": "গুগল দিয়ে লগ ইন করুন", + "User ID": "ইউজার আইডি", + "Password": "পাসওয়ার্ড", + "Time (h:mm:ss):": "সময় (ঘণ্টা:মিনিট:সেকেন্ড):", + "Text CAPTCHA": "টেক্সট ক্যাপচা", + "Image CAPTCHA": "চিত্র ক্যাপচা", + "Sign In": "সাইন ইন", + "Register": "নিবন্ধন", + "E-mail": "ই-মেইল", + "Google verification code": "গুগল যাচাইকরণ কোড", + "Preferences": "পছন্দসমূহ", + "Player preferences": "প্লেয়ারের পছন্দসমূহ", + "Always loop: ": "সর্বদা লুপ: ", + "Autoplay: ": "স্বয়ংক্রিয় চালু: ", + "Play next by default: ": "ডিফল্টভাবে পরবর্তী চালাও: ", + "Autoplay next video: ": "পরবর্তী ভিডিও স্বয়ংক্রিয়ভাবে চালাও: ", + "Listen by default: ": "", + "Proxy videos: ": "", + "Default speed: ": "", + "Preferred video quality: ": "", + "Player volume: ": "", + "Default comments: ": "", + "youtube": "", + "reddit": "", + "Default captions: ": "", + "Fallback captions: ": "", + "Show related videos: ": "", + "Show annotations by default: ": "", + "Visual preferences": "", + "Player style: ": "", + "Dark mode: ": "", + "Theme: ": "", + "dark": "", + "light": "", + "Thin mode: ": "", + "Subscription preferences": "", + "Show annotations by default for subscribed channels: ": "", + "Redirect homepage to feed: ": "", + "Number of videos shown in feed: ": "", + "Sort videos by: ": "", + "published": "", + "published - reverse": "", + "alphabetically": "", + "alphabetically - reverse": "", + "channel name": "", + "channel name - reverse": "", + "Only show latest video from channel: ": "", + "Only show latest unwatched video from channel: ": "", + "Only show unwatched: ": "", + "Only show notifications (if there are any): ": "", + "Enable web notifications": "", + "`x` uploaded a video": "", + "`x` is live": "", + "Data preferences": "", + "Clear watch history": "", + "Import/export data": "", + "Change password": "", + "Manage subscriptions": "", + "Manage tokens": "", + "Watch history": "", + "Delete account": "", + "Administrator preferences": "", + "Default homepage: ": "", + "Feed menu: ": "", + "Top enabled: ": "", + "CAPTCHA enabled: ": "", + "Login enabled: ": "", + "Registration enabled: ": "", + "Report statistics: ": "", + "Save preferences": "", + "Subscription manager": "", + "Token manager": "", + "Token": "", + "`x` subscriptions.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` subscriptions.": "", + "`x` tokens.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` tokens.": "", + "Import/export": "", + "unsubscribe": "", + "revoke": "", + "Subscriptions": "", + "`x` unseen notifications.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` unseen notifications.": "", + "search": "", + "Log out": "", + "Released under the AGPLv3 by Omar Roth.": "", + "Source available here.": "", + "View JavaScript license information.": "", + "View privacy policy.": "", + "Trending": "", + "Public": "", + "Unlisted": "", + "Private": "", + "View all playlists": "", + "Updated `x` ago": "", + "Delete playlist `x`?": "", + "Delete playlist": "", + "Create playlist": "", + "Title": "", + "Playlist privacy": "", + "Editing playlist `x`": "", + "Watch on YouTube": "", + "Hide annotations": "", + "Show annotations": "", + "Genre: ": "", + "License: ": "", + "Family friendly? ": "", + "Wilson score: ": "", + "Engagement: ": "", + "Whitelisted regions: ": "", + "Blacklisted regions: ": "", + "Shared `x`": "", + "`x` views.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` views.": "", + "Premieres in `x`": "", + "Premieres `x`": "", + "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "", + "View YouTube comments": "", + "View more comments on Reddit": "", + "View `x` comments.([^.,0-9]|^)1([^.,0-9]|$)": "", + "View `x` comments.": "", + "View Reddit comments": "", + "Hide replies": "", + "Show replies": "", + "Incorrect password": "", + "Quota exceeded, try again in a few hours": "", + "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "", + "Invalid TFA code": "", + "Login failed. This may be because two-factor authentication is not turned on for your account.": "", + "Wrong answer": "", + "Erroneous CAPTCHA": "", + "CAPTCHA is a required field": "", + "User ID is a required field": "", + "Password is a required field": "", + "Wrong username or password": "", + "Please sign in using 'Log in with Google'": "", + "Password cannot be empty": "", + "Password cannot be longer than 55 characters": "", + "Please log in": "", + "Invidious Private Feed for `x`": "", + "channel:`x`": "", + "Deleted or invalid channel": "", + "This channel does not exist.": "", + "Could not get channel info.": "", + "Could not fetch comments": "", + "View `x` replies.([^.,0-9]|^)1([^.,0-9]|$)": "", + "View `x` replies.": "", + "`x` ago": "", + "Load more": "", + "`x` points.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` points.": "", + "Could not create mix.": "", + "Empty playlist": "", + "Not a playlist.": "", + "Playlist does not exist.": "", + "Could not pull trending pages.": "", + "Hidden field \"challenge\" is a required field": "", + "Hidden field \"token\" is a required field": "", + "Erroneous challenge": "", + "Erroneous token": "", + "No such user": "", + "Token is expired, please try again": "", + "English": "", + "English (auto-generated)": "", + "Afrikaans": "", + "Albanian": "", + "Amharic": "", + "Arabic": "", + "Armenian": "", + "Azerbaijani": "", + "Bangla": "", + "Basque": "", + "Belarusian": "", + "Bosnian": "", + "Bulgarian": "", + "Burmese": "", + "Catalan": "", + "Cebuano": "", + "Chinese (Simplified)": "", + "Chinese (Traditional)": "", + "Corsican": "", + "Croatian": "", + "Czech": "", + "Danish": "", + "Dutch": "", + "Esperanto": "", + "Estonian": "", + "Filipino": "", + "Finnish": "", + "French": "", + "Galician": "", + "Georgian": "", + "German": "", + "Greek": "", + "Gujarati": "", + "Haitian Creole": "", + "Hausa": "", + "Hawaiian": "", + "Hebrew": "", + "Hindi": "", + "Hmong": "", + "Hungarian": "", + "Icelandic": "", + "Igbo": "", + "Indonesian": "", + "Irish": "", + "Italian": "", + "Japanese": "", + "Javanese": "", + "Kannada": "", + "Kazakh": "", + "Khmer": "", + "Korean": "", + "Kurdish": "", + "Kyrgyz": "", + "Lao": "", + "Latin": "", + "Latvian": "", + "Lithuanian": "", + "Luxembourgish": "", + "Macedonian": "", + "Malagasy": "", + "Malay": "", + "Malayalam": "", + "Maltese": "", + "Maori": "", + "Marathi": "", + "Mongolian": "", + "Nepali": "", + "Norwegian Bokmål": "", + "Nyanja": "", + "Pashto": "", + "Persian": "", + "Polish": "", + "Portuguese": "", + "Punjabi": "", + "Romanian": "", + "Russian": "", + "Samoan": "", + "Scottish Gaelic": "", + "Serbian": "", + "Shona": "", + "Sindhi": "", + "Sinhala": "", + "Slovak": "", + "Slovenian": "", + "Somali": "", + "Southern Sotho": "", + "Spanish": "", + "Spanish (Latin America)": "", + "Sundanese": "", + "Swahili": "", + "Swedish": "", + "Tajik": "", + "Tamil": "", + "Telugu": "", + "Thai": "", + "Turkish": "", + "Ukrainian": "", + "Urdu": "", + "Uzbek": "", + "Vietnamese": "", + "Welsh": "", + "Western Frisian": "", + "Xhosa": "", + "Yiddish": "", + "Yoruba": "", + "Zulu": "", + "`x` years.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` years.": "", + "`x` months.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` months.": "", + "`x` weeks.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` weeks.": "", + "`x` days.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` days.": "", + "`x` hours.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` hours.": "", + "`x` minutes.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` minutes.": "", + "`x` seconds.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` seconds.": "", + "Fallback comments: ": "", + "Popular": "", + "Top": "", + "About": "", + "Rating: ": "", + "Language: ": "", + "View as playlist": "", + "Default": "", + "Music": "", + "Gaming": "", + "News": "", + "Movies": "", + "Download": "", + "Download as: ": "", + "%A %B %-d, %Y": "", + "(edited)": "", + "YouTube comment permalink": "", + "permalink": "", + "`x` marked it with a ❤": "", + "Audio mode": "", + "Video mode": "", + "Videos": "", + "Playlists": "", + "Community": "", + "Current version: ": "" +} diff --git a/locales/da.json b/locales/da.json new file mode 100644 index 00000000..1944e47b --- /dev/null +++ b/locales/da.json @@ -0,0 +1,387 @@ +{ + "`x` subscribers": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` abonnenter.([^.,0-9]|^)1([^.,0-9]|$)", + "": "`x` abonnenter." + }, + "`x` videos": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` videoer.([^.,0-9]|^)1([^.,0-9]|$)", + "": "`x` videoer." + }, + "`x` playlists": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` afspilningslister.([^.,0-9]|^)1([^.,0-9]|$)", + "": "`x` afspilningslister." + }, + "LIVE": "DIREKTE", + "Shared `x` ago": "Delt for `x` siden", + "Unsubscribe": "", + "Subscribe": "Abonner", + "View channel on YouTube": "Vis kanal på YouTube", + "View playlist on YouTube": "Vis afspilningsliste på YouTube", + "newest": "nyeste", + "oldest": "ældste", + "popular": "populært", + "last": "sidste", + "Next page": "Næste side", + "Previous page": "Forrige side", + "Clear watch history?": "Ryd afspilningshistorik?", + "New password": "Nyt kodeord", + "New passwords must match": "Nye kodeord skal matche", + "Cannot change password for Google accounts": "Kan ikke skifte kodeord til Google-konti", + "Authorize token?": "Godkend token?", + "Authorize token for `x`?": "Godkende token til `x`?", + "Yes": "Ja", + "No": "Nej", + "Import and Export Data": "Importer og Eksporter Data", + "Import": "Importer", + "Import Invidious data": "Importer Invidious data", + "Import YouTube subscriptions": "Importer Youtube abonnementer", + "Import FreeTube subscriptions (.db)": "Importer FreeTube abonnementer (.db)", + "Import NewPipe subscriptions (.json)": "Importer NewPipe abonnementer (.json)", + "Import NewPipe data (.zip)": "Importer NewPipe data (.zip)", + "Export": "Exporter", + "Export subscriptions as OPML": "Exporter abonnementer som OPML", + "Export subscriptions as OPML (for NewPipe & FreeTube)": "Exporter abonnementer som OPML (til NewPipe & FreeTube)", + "Export data as JSON": "Exporter data som JSON", + "Delete account?": "Slet konto?", + "History": "Historik", + "An alternative front-end to YouTube": "", + "JavaScript license information": "JavaScript licens information", + "source": "kilde", + "Log in": "Log på", + "Log in/register": "Log på/registrer", + "Log in with Google": "Log på med Google", + "User ID": "Bruger ID", + "Password": "Kodeord", + "Time (h:mm:ss):": "Tid (t:mm:ss):", + "Text CAPTCHA": "Tekst CAPTCHA", + "Image CAPTCHA": "Billede CAPTCHA", + "Sign In": "Log ind", + "Register": "Registrer", + "E-mail": "E-mail", + "Google verification code": "Google verifications kode", + "Preferences": "Præferencer", + "Player preferences": "", + "Always loop: ": "Altid gentag: ", + "Autoplay: ": "Auto afspil: ", + "Play next by default: ": "Afspil næste som standard: ", + "Autoplay next video: ": "Auto afspil næste video: ", + "Listen by default: ": "Lyt som standard: ", + "Proxy videos: ": "Proxy videoer: ", + "Default speed: ": "Standard hastighed: ", + "Preferred video quality: ": "Foretrukken video kvalitet: ", + "Player volume: ": "Lydstyrke: ", + "Default comments: ": "Standard kommentarer: ", + "youtube": "youtube", + "reddit": "reddit", + "Default captions: ": "", + "Fallback captions: ": "", + "Show related videos: ": "", + "Show annotations by default: ": "", + "Visual preferences": "", + "Player style: ": "", + "Dark mode: ": "", + "Theme: ": "", + "dark": "", + "light": "", + "Thin mode: ": "", + "Subscription preferences": "", + "Show annotations by default for subscribed channels: ": "", + "Redirect homepage to feed: ": "", + "Number of videos shown in feed: ": "", + "Sort videos by: ": "", + "published": "", + "published - reverse": "", + "alphabetically": "", + "alphabetically - reverse": "", + "channel name": "", + "channel name - reverse": "", + "Only show latest video from channel: ": "", + "Only show latest unwatched video from channel: ": "", + "Only show unwatched: ": "", + "Only show notifications (if there are any): ": "", + "Enable web notifications": "", + "`x` uploaded a video": "", + "`x` is live": "", + "Data preferences": "", + "Clear watch history": "", + "Import/export data": "", + "Change password": "", + "Manage subscriptions": "", + "Manage tokens": "", + "Watch history": "", + "Delete account": "", + "Administrator preferences": "", + "Default homepage: ": "", + "Feed menu: ": "", + "Top enabled: ": "", + "CAPTCHA enabled: ": "", + "Login enabled: ": "", + "Registration enabled: ": "", + "Report statistics: ": "", + "Save preferences": "", + "Subscription manager": "", + "Token manager": "", + "Token": "", + "`x` subscriptions": { + "([^.,0-9]|^)1([^.,0-9]|$)": "", + "": "" + }, + "`x` tokens": { + "([^.,0-9]|^)1([^.,0-9]|$)": "", + "": "" + }, + "Import/export": "", + "unsubscribe": "", + "revoke": "", + "Subscriptions": "", + "`x` unseen notifications": { + "([^.,0-9]|^)1([^.,0-9]|$)": "", + "": "" + }, + "search": "", + "Log out": "", + "Released under the AGPLv3 by Omar Roth.": "", + "Source available here.": "", + "View JavaScript license information.": "", + "View privacy policy.": "", + "Trending": "", + "Public": "", + "Unlisted": "", + "Private": "", + "View all playlists": "", + "Updated `x` ago": "", + "Delete playlist `x`?": "", + "Delete playlist": "", + "Create playlist": "", + "Title": "", + "Playlist privacy": "", + "Editing playlist `x`": "", + "Watch on YouTube": "", + "Hide annotations": "", + "Show annotations": "", + "Genre: ": "", + "License: ": "", + "Family friendly? ": "", + "Wilson score: ": "", + "Engagement: ": "", + "Whitelisted regions: ": "", + "Blacklisted regions: ": "", + "Shared `x`": "", + "`x` views": { + "([^.,0-9]|^)1([^.,0-9]|$)": "", + "": "" + }, + "Premieres in `x`": "", + "Premieres `x`": "", + "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "", + "View YouTube comments": "", + "View more comments on Reddit": "", + "View `x` comments": { + "([^.,0-9]|^)1([^.,0-9]|$)": "", + "": "" + }, + "View Reddit comments": "", + "Hide replies": "", + "Show replies": "", + "Incorrect password": "", + "Quota exceeded, try again in a few hours": "", + "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "", + "Invalid TFA code": "", + "Login failed. This may be because two-factor authentication is not turned on for your account.": "", + "Wrong answer": "", + "Erroneous CAPTCHA": "", + "CAPTCHA is a required field": "", + "User ID is a required field": "", + "Password is a required field": "", + "Wrong username or password": "", + "Please sign in using 'Log in with Google'": "", + "Password cannot be empty": "", + "Password cannot be longer than 55 characters": "", + "Please log in": "", + "Invidious Private Feed for `x`": "", + "channel:`x`": "", + "Deleted or invalid channel": "", + "This channel does not exist.": "", + "Could not get channel info.": "", + "Could not fetch comments": "", + "View `x` replies": { + "([^.,0-9]|^)1([^.,0-9]|$)": "", + "": "" + }, + "`x` ago": "", + "Load more": "", + "`x` points": { + "([^.,0-9]|^)1([^.,0-9]|$)": "", + "": "" + }, + "Could not create mix.": "", + "Empty playlist": "", + "Not a playlist.": "", + "Playlist does not exist.": "", + "Could not pull trending pages.": "", + "Hidden field \"challenge\" is a required field": "", + "Hidden field \"token\" is a required field": "", + "Erroneous challenge": "", + "Erroneous token": "", + "No such user": "", + "Token is expired, please try again": "", + "English": "", + "English (auto-generated)": "", + "Afrikaans": "", + "Albanian": "", + "Amharic": "", + "Arabic": "", + "Armenian": "", + "Azerbaijani": "", + "Bangla": "", + "Basque": "", + "Belarusian": "", + "Bosnian": "", + "Bulgarian": "", + "Burmese": "", + "Catalan": "", + "Cebuano": "", + "Chinese (Simplified)": "", + "Chinese (Traditional)": "", + "Corsican": "", + "Croatian": "", + "Czech": "", + "Danish": "", + "Dutch": "", + "Esperanto": "", + "Estonian": "", + "Filipino": "", + "Finnish": "", + "French": "", + "Galician": "", + "Georgian": "", + "German": "", + "Greek": "", + "Gujarati": "", + "Haitian Creole": "", + "Hausa": "", + "Hawaiian": "", + "Hebrew": "", + "Hindi": "", + "Hmong": "", + "Hungarian": "", + "Icelandic": "", + "Igbo": "", + "Indonesian": "", + "Irish": "", + "Italian": "", + "Japanese": "", + "Javanese": "", + "Kannada": "", + "Kazakh": "", + "Khmer": "", + "Korean": "", + "Kurdish": "", + "Kyrgyz": "", + "Lao": "", + "Latin": "", + "Latvian": "", + "Lithuanian": "", + "Luxembourgish": "", + "Macedonian": "", + "Malagasy": "", + "Malay": "", + "Malayalam": "", + "Maltese": "", + "Maori": "", + "Marathi": "", + "Mongolian": "", + "Nepali": "", + "Norwegian Bokmål": "", + "Nyanja": "", + "Pashto": "", + "Persian": "", + "Polish": "", + "Portuguese": "", + "Punjabi": "", + "Romanian": "", + "Russian": "", + "Samoan": "", + "Scottish Gaelic": "", + "Serbian": "", + "Shona": "", + "Sindhi": "", + "Sinhala": "", + "Slovak": "", + "Slovenian": "", + "Somali": "", + "Southern Sotho": "", + "Spanish": "", + "Spanish (Latin America)": "", + "Sundanese": "", + "Swahili": "", + "Swedish": "", + "Tajik": "", + "Tamil": "", + "Telugu": "", + "Thai": "", + "Turkish": "", + "Ukrainian": "", + "Urdu": "", + "Uzbek": "", + "Vietnamese": "", + "Welsh": "", + "Western Frisian": "", + "Xhosa": "", + "Yiddish": "", + "Yoruba": "", + "Zulu": "", + "`x` years": { + "([^.,0-9]|^)1([^.,0-9]|$)": "", + "": "" + }, + "`x` months": { + "([^.,0-9]|^)1([^.,0-9]|$)": "", + "": "" + }, + "`x` weeks": { + "([^.,0-9]|^)1([^.,0-9]|$)": "", + "": "" + }, + "`x` days": { + "([^.,0-9]|^)1([^.,0-9]|$)": "", + "": "" + }, + "`x` hours": { + "([^.,0-9]|^)1([^.,0-9]|$)": "", + "": "" + }, + "`x` minutes": { + "([^.,0-9]|^)1([^.,0-9]|$)": "", + "": "" + }, + "`x` seconds": { + "([^.,0-9]|^)1([^.,0-9]|$)": "", + "": "" + }, + "Fallback comments: ": "", + "Popular": "", + "Top": "", + "About": "", + "Rating: ": "", + "Language: ": "", + "View as playlist": "", + "Default": "", + "Music": "", + "Gaming": "", + "News": "", + "Movies": "", + "Download": "", + "Download as: ": "", + "%A %B %-d, %Y": "", + "(edited)": "", + "YouTube comment permalink": "", + "permalink": "", + "`x` marked it with a ❤": "", + "Audio mode": "", + "Video mode": "", + "Videos": "", + "Playlists": "", + "Community": "", + "Current version: ": "" +} diff --git a/locales/de.json b/locales/de.json index b685a842..b995baac 100644 --- a/locales/de.json +++ b/locales/de.json @@ -33,14 +33,14 @@ "Export subscriptions as OPML": "Abonnements als OPML exportieren", "Export subscriptions as OPML (for NewPipe & FreeTube)": "Abonnements als OPML exportieren (für NewPipe & FreeTube)", "Export data as JSON": "Daten als JSON exportieren", - "Delete account?": "Account löschen?", + "Delete account?": "Konto löschen?", "History": "Verlauf", "An alternative front-end to YouTube": "Eine alternative Oberfläche für YouTube", "JavaScript license information": "JavaScript Lizenzinformationen", "source": "Quelle", - "Log in": "Einloggen", - "Log in/register": "Einloggen/Registrieren", - "Log in with Google": "Mit Google einloggen", + "Log in": "Anmelden", + "Log in/register": "Anmelden/registrieren", + "Log in with Google": "Mit Google anmelden", "User ID": "Benutzer ID", "Password": "Passwort", "Time (h:mm:ss):": "Zeit (h:mm:ss):", @@ -106,7 +106,7 @@ "Feed menu: ": "Feed-Menü: ", "Top enabled: ": "Top aktiviert? ", "CAPTCHA enabled: ": "CAPTCHA aktiviert? ", - "Login enabled: ": "Login aktiviert? ", + "Login enabled: ": "Anmeldung aktiviert: ", "Registration enabled: ": "Registrierung aktiviert? ", "Report statistics: ": "Statistiken berichten? ", "Save preferences": "Einstellungen speichern", @@ -161,16 +161,16 @@ "Show replies": "Antworten anzeigen", "Incorrect password": "Falsches Passwort", "Quota exceeded, try again in a few hours": "Kontingent überschritten, versuche es in ein paar Stunden erneut", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Login nicht möglich, stellen Sie sicher dass two-factor Authentifikation (Authentifizierung oder SMS) aktiviert ist.", + "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Anmeldung nicht möglich, stellen Sie sicher, dass die Zwei-Faktor-Authentisierung (Authenticator oder SMS) aktiviert ist.", "Invalid TFA code": "Ungültiger TFA Code", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "Login fehlgeschlagen. Das kann daran liegen dass two-factor Authentifizierung in ihrem Account nicht aktiviert ist.", + "Login failed. This may be because two-factor authentication is not turned on for your account.": "Die Anmeldung ist fehlgeschlagen. Dies kann daran liegen, dass die Zwei-Faktor-Authentisierung für Ihr Konto nicht aktiviert ist.", "Wrong answer": "Ungültige Antwort", "Erroneous CAPTCHA": "Ungültiges CAPTCHA", "CAPTCHA is a required field": "CAPTCHA ist eine erforderliche Eingabe", "User ID is a required field": "Benutzer ID ist eine erforderliche Eingabe", "Password is a required field": "Passwort ist eine erforderliche Eingabe", "Wrong username or password": "Ungültiger Benutzername oder Passwort", - "Please sign in using 'Log in with Google'": "Bitte melden sie sich mit 'Mit Google anmelden' an", + "Please sign in using 'Log in with Google'": "Bitte melden Sie sich mit „Mit Google anmelden“ an", "Password cannot be empty": "Passwort darf nicht leer sein", "Password cannot be longer than 55 characters": "Passwort darf nicht länger als 55 Zeichen sein", "Please log in": "Bitte anmelden", @@ -189,8 +189,8 @@ "Not a playlist.": "Ungültige Playlist.", "Playlist does not exist.": "Playlist existiert nicht.", "Could not pull trending pages.": "Trending Seiten konnten nicht geladen werden.", - "Hidden field \"challenge\" is a required field": "Verstecktes Feld \"challenge\" ist eine erforderliche Eingabe", - "Hidden field \"token\" is a required field": "Verstecktes Feld \"token\" ist eine erforderliche Eingabe", + "Hidden field \"challenge\" is a required field": "Verstecktes Feld „challenge“ ist eine erforderliche Eingabe", + "Hidden field \"token\" is a required field": "Verstecktes Feld „token“ ist eine erforderliche Eingabe", "Erroneous challenge": "Ungültiger Test", "Erroneous token": "Ungültiger Token", "No such user": "Ungültiger Benutzer", @@ -322,7 +322,7 @@ "Movies": "Filme", "Download": "Herunterladen", "Download as: ": "Herunterladen als: ", - "%A %B %-d, %Y": "%A %B %-d, %Y", + "%A %B %-d, %Y": "%A %-d %B %Y", "(edited)": "(editiert)", "YouTube comment permalink": "YouTube-Kommentar Permalink", "permalink": "Permalink", @@ -333,4 +333,4 @@ "Playlists": "Wiedergabelisten", "Community": "Gemeinschaft", "Current version: ": "Aktuelle Version: " -} \ No newline at end of file +} diff --git a/locales/el.json b/locales/el.json index f4249ebc..23b3cdf9 100644 --- a/locales/el.json +++ b/locales/el.json @@ -1,19 +1,15 @@ { - "`x` subscribers": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` συνδρομητής", - "": "`x` συνδρομητές" - }, - "`x` videos": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` βίντεο", - "": "`x` βίντεο" - }, - "`x` playlists": "", + "`x` subscribers.([^.,0-9]|^)1([^.,0-9]|$)": "`x` συνδρομητής", + "`x` subscribers.": "`x` συνδρομητές.", + "`x` videos.([^.,0-9]|^)1([^.,0-9]|$)": "`x` βίντεο", + "`x` videos.": "`x` βίντεο.", + "`x` playlists": "`x` κατάλογοι αναπαραγωγής", "LIVE": "ΖΩΝΤΑΝΑ", "Shared `x` ago": "Μοιράστηκε πριν `x`", "Unsubscribe": "Απεγγραφή", "Subscribe": "Εγγραφή", "View channel on YouTube": "Προβολή καναλιού στο YouTube", - "View playlist on YouTube": "", + "View playlist on YouTube": "Προβολή καταλόγου αναπαραγωγής στο YouTube", "newest": "νεότερα", "oldest": "παλιότερα", "popular": "δημοφιλή", @@ -54,7 +50,7 @@ "Image CAPTCHA": "Εικόνα CAPTCHA", "Sign In": "Σύνδεση", "Register": "Εγγραφή", - "E-mail": "E-mail", + "E-mail": "Ηλεκτρονικό ταχυδρομείο", "Google verification code": "Κωδικός επαλήθευσης Google", "Preferences": "Προτιμήσεις", "Player preferences": "Προτιμήσεις αναπαραγωγής", @@ -68,18 +64,18 @@ "Preferred video quality: ": "Προτιμώμενη ανάλυση: ", "Player volume: ": "Ένταση αναπαραγωγής: ", "Default comments: ": "Προεπιλεγμένα σχόλια: ", - "youtube": "youtube", + "youtube": "YouTube", "reddit": "reddit", "Default captions: ": "Προεπιλεγμένοι υπότιτλοι: ", "Fallback captions: ": "Εναλλακτικοί υπότιτλοι: ", "Show related videos: ": "Προβολή σχετικών βίντεο; ", - "Show annotations by default: ": "Αυτόματη προβολή σημειώσεων; :", + "Show annotations by default: ": "Αυτόματη προβολή σημειώσεων: ", "Visual preferences": "Προτιμήσεις εμφάνισης", - "Player style: ": "", + "Player style: ": "Τεχνοτροπία της συσκευής αναπαραγωγης: ", "Dark mode: ": "Σκοτεινή λειτουργία: ", - "Theme: ": "", - "dark": "", - "light": "", + "Theme: ": "Θέμα: ", + "dark": "σκοτεινό", + "light": "φωτεινό", "Thin mode: ": "Ελαφριά λειτουργία: ", "Subscription preferences": "Προτιμήσεις συνδρομών", "Show annotations by default for subscribed channels: ": "Προβολή σημειώσεων μόνο για κανάλια στα οποία είστε συνδρομητής; ", @@ -96,9 +92,9 @@ "Only show latest unwatched video from channel: ": "Προβολή μόνο του τελευταίου μη-προβεβλημένου βίντεο του καναλιού: ", "Only show unwatched: ": "Προβολή μόνο μη-προβεβλημένων: ", "Only show notifications (if there are any): ": "Προβολή μόνο ειδοποιήσεων (αν υπάρχουν): ", - "Enable web notifications": "", - "`x` uploaded a video": "", - "`x` is live": "", + "Enable web notifications": "Ενεργοποίηση ειδοποιήσεων δικτύου", + "`x` uploaded a video": "`x` κοινοποίησε ένα βίντεο", + "`x` is live": "`x` κάνει live", "Data preferences": "Προτιμήσεις δεδομένων", "Clear watch history": "Εκκαθάριση ιστορικού προβολής", "Import/export data": "Εισαγωγή/εξαγωγή δεδομένων", @@ -119,22 +115,16 @@ "Subscription manager": "Διαχειριστής συνδρομών", "Token manager": "Διαχειριστής διασυνδέσεων", "Token": "Διασύνδεση", - "`x` subscriptions": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` συνδρομή", - "": "`x` συνδρομές" - }, - "`x` tokens": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` διασύνδεση", - "": "`x` διασυνδέσεις" - }, + "`x` subscriptions.([^.,0-9]|^)1([^.,0-9]|$)": "`x` συνδρομή", + "`x` subscriptions.": "`x` συνδρομές.", + "`x` tokens.([^.,0-9]|^)1([^.,0-9]|$)": "`x` διασύνδεση", + "`x` tokens.": "`x` διασυνδέσεις.", "Import/export": "Εισαγωγή/εξαγωγή", "unsubscribe": "κατάργηση συνδρομής", "revoke": "ανάκληση", "Subscriptions": "Συνδρομές", - "`x` unseen notifications": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` καινούρια ειδοποίηση", - "": "`x` καινούριες ειδοποιήσεις" - }, + "`x` unseen notifications.([^.,0-9]|^)1([^.,0-9]|$)": "`x` καινούρια ειδοποίηση", + "`x` unseen notifications.": "`x` καινούριες ειδοποιήσεις.", "search": "αναζήτηση", "Log out": "Αποσύνδεση", "Released under the AGPLv3 by Omar Roth.": "Κυκλοφορεί υπό την άδεια AGPLv3 από τον Omar Roth.", @@ -142,35 +132,33 @@ "View JavaScript license information.": "Προβολή πληροφοριών άδειας JavaScript.", "View privacy policy.": "Προβολή πολιτικής απορρήτου.", "Trending": "Τάσεις", - "Public": "", + "Public": "Δημόσιο", "Unlisted": "Κρυφό", - "Private": "", - "View all playlists": "", - "Updated `x` ago": "", - "Delete playlist `x`?": "", - "Delete playlist": "", - "Create playlist": "", - "Title": "", - "Playlist privacy": "", - "Editing playlist `x`": "", + "Private": "Ιδιωτικό", + "View all playlists": "Προβολή όλων των καταλόγων αναπαραγωγής", + "Updated `x` ago": "Ενημερώθηκε πριν από `x`", + "Delete playlist `x`?": "Διαγραφή `x` καταλόγου αναπαραγωγής;", + "Delete playlist": "Διαγραφή καταλόγου αναπαραγωγής", + "Create playlist": "Δημιουργία καταλόγου αναπαραγωγής", + "Title": "Τίτλος", + "Playlist privacy": "Ιδιωτικότητα καταλόγων αναπαραγωγής", + "Editing playlist `x`": "Επεξεργασία `x` καταλόγου αναπαραγωγής", "Watch on YouTube": "Προβολή στο YouTube", "Hide annotations": "Απόκρυψη σημειώσεων", "Show annotations": "Προβολή σημειώσεων", "Genre: ": "Είδος: ", "License: ": "Άδεια: ", "Family friendly? ": "Φιλικό προς την οικογένεια; ", - "Wilson score: ": "Wilson score: ", + "Wilson score: ": "Αποτελέσματα Wilson: ", "Engagement: ": "Ενδιαφέρον: ", "Whitelisted regions: ": "Επιτρεπτές περιοχές: ", "Blacklisted regions: ": "Μη-επιτρεπτές περιοχές: ", "Shared `x`": "Μοιράστηκε το `x`", - "`x` views": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` προβολή", - "": "`x` προβολές" - }, + "`x` views.([^.,0-9]|^)1([^.,0-9]|$)": "`x` προβολή", + "`x` views.": "`x` προβολές.", "Premieres in `x`": "Πρώτη προβολή σε `x`", - "Premieres `x`": "", - "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Γεια! Φαίνεται πως έχετε απενεργοποιήσει το JavaScript. Πατήστε εδώ για προβολή σχολίων, αλλά έχετε υπ'όψιν σας πως ίσως φορτώσουν πιο αργά. ", + "Premieres `x`": "Επίσημη πρώτη παράσταση του `x`", + "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Γεια! Φαίνεται πως έχετε απενεργοποιήσει το JavaScript. Πατήστε εδώ για προβολή σχολίων, αλλά έχετε υπ'όψιν σας πως ίσως φορτώσουν πιο αργά.", "View YouTube comments": "Προβολή σχολίων από το YouTube", "View more comments on Reddit": "Προβολή περισσότερων σχολίων στο Reddit", "View `x` comments": "Προβολή `x` σχολίων", @@ -198,19 +186,15 @@ "This channel does not exist.": "Αυτό το κανάλι δεν υπάρχει.", "Could not get channel info.": "Αδύναμια εύρεσης πληροφοριών καναλιού.", "Could not fetch comments": "Αδυναμία λήψης σχολίων", - "View `x` replies": { - "([^.,0-9]|^)1([^.,0-9]|$)": "Προβολή `x` απάντησης", - "": "Προβολή `x` απαντήσεων" - }, + "View `x` replies.([^.,0-9]|^)1([^.,0-9]|$)": "Προβολή `x` απάντησης", + "View `x` replies.": "Προβολή `x` απαντήσεων.", "`x` ago": "Πριν `x`", "Load more": "Φόρτωση περισσότερων", - "`x` points": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` βαθμός", - "": "`x` βαθμοί" - }, + "`x` points.([^.,0-9]|^)1([^.,0-9]|$)": "`x` βαθμός", + "`x` points.": "`x` βαθμοί.", "Could not create mix.": "Αδυναμία δημιουργίας μίξης.", "Empty playlist": "Κενή λίστα αναπαραγωγής", - "Not a playlist.": "Μη έγκυρη λίστα αναπαραγωγής", + "Not a playlist.": "Μη έγκυρη λίστα αναπαραγωγής.", "Playlist does not exist.": "Μη υπαρκτή λίστα αναπαραγωγής.", "Could not pull trending pages.": "Αδυναμία λήψης σελίδας τάσεων.", "Hidden field \"challenge\" is a required field": "Το Κρυφό πεδίο \"δοκιμασία\" είναι απαραίτητο", @@ -325,34 +309,20 @@ "Yiddish": "Γίντις", "Yoruba": "Γιορούμπα", "Zulu": "Ζουλού", - "`x` years": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` χρόνο", - "": "`x` χρόνια" - }, - "`x` months": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` μήνα", - "": "`x` μήνες" - }, - "`x` weeks": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` εβδομάδα", - "": "`x` εβδομάδες" - }, - "`x` days": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` ημέρα", - "": "`x` ημέρες" - }, - "`x` hours": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` ώρα", - "": "`x` ώρες" - }, - "`x` minutes": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` λεπτό", - "": "`x` λεπτά" - }, - "`x` seconds": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` δευτερόλεπτο", - "": "`x` δευτερόλεπτα" - }, + "`x` years.([^.,0-9]|^)1([^.,0-9]|$)": "`x` χρόνο", + "`x` years.": "`x` χρόνια.", + "`x` months.([^.,0-9]|^)1([^.,0-9]|$)": "`x` μήνα", + "`x` months.": "`x` μήνες.", + "`x` weeks.([^.,0-9]|^)1([^.,0-9]|$)": "`x` εβδομάδα", + "`x` weeks.": "`x` εβδομάδες.", + "`x` days.([^.,0-9]|^)1([^.,0-9]|$)": "`x` ημέρα", + "`x` days.": "`x` ημέρες.", + "`x` hours.([^.,0-9]|^)1([^.,0-9]|$)": "`x` ώρα", + "`x` hours.": "`x` ώρες.", + "`x` minutes.([^.,0-9]|^)1([^.,0-9]|$)": "`x` λεπτό", + "`x` minutes.": "`x` λεπτά.", + "`x` seconds.([^.,0-9]|^)1([^.,0-9]|$)": "`x` δευτερόλεπτο", + "`x` seconds.": "`x` δευτερόλεπτα.", "Fallback comments: ": "Εναλλακτικά σχόλια: ", "Popular": "Δημοφιλή", "Top": "Κορυφαία", @@ -370,12 +340,12 @@ "%A %B %-d, %Y": "%A %B %-d, %Y", "(edited)": "(τροποποιημένο)", "YouTube comment permalink": "Σύνδεσμος YouTube σχολίου", - "permalink": "", + "permalink": "μόνιμος σύνδεσμος", "`x` marked it with a ❤": "Ο χρηστης `x` έβαλε ❤", "Audio mode": "Λειτουργία ήχου", "Video mode": "Λειτουργία βίντεο", "Videos": "Βίντεο", "Playlists": "Λίστες Αναπαραγωγής", - "Community": "", + "Community": "Κοινότητα", "Current version: ": "Τρέχουσα έκδοση: " -} \ No newline at end of file +} diff --git a/locales/eo.json b/locales/eo.json index ae640e37..3052ac35 100644 --- a/locales/eo.json +++ b/locales/eo.json @@ -4,10 +4,10 @@ "`x` playlists": "`x` ludlistoj", "LIVE": "NUNA", "Shared `x` ago": "Konigita antaŭ `x`", - "Unsubscribe": "Malaboni", - "Subscribe": "Aboni", - "View channel on YouTube": "Vidi kanalon en JuTubo", - "View playlist on YouTube": "Vidi ludliston en JuTubo", + "Unsubscribe": "Malabonu", + "Subscribe": "Abonu", + "View channel on YouTube": "Vidu kanalon en JuTubo", + "View playlist on YouTube": "Vidu ludliston en JuTubo", "newest": "pli novaj", "oldest": "pli malnovaj", "popular": "popularaj", @@ -116,7 +116,7 @@ "`x` subscriptions": "`x` abonoj", "`x` tokens": "`x` ĵetonoj", "Import/export": "Importi/Eksporti", - "unsubscribe": "malaboni", + "unsubscribe": "malabonu", "revoke": "senvalidigi", "Subscriptions": "Abonoj", "`x` unseen notifications": "`x` neviditaj sciigoj", @@ -316,7 +316,7 @@ "Language: ": "Lingvo: ", "View as playlist": "Vidi kiel ludlisto", "Default": "Defaŭlte", - "Music": "Musiko", + "Music": "Muziko", "Gaming": "Komputiloludoj", "News": "Novaĵoj", "Movies": "Filmoj", @@ -333,4 +333,4 @@ "Playlists": "Ludlistoj", "Community": "Komunumo", "Current version: ": "Nuna versio: " -} \ No newline at end of file +} diff --git a/locales/es.json b/locales/es.json index 7fc75003..91faef1b 100644 --- a/locales/es.json +++ b/locales/es.json @@ -132,8 +132,8 @@ "Private": "Privado", "View all playlists": "Ver todas las listas de reproducción", "Updated `x` ago": "Actualizado hace `x`", - "Delete playlist `x`?": "¿Eliminar la lista de reproducción `x`?", - "Delete playlist": "Eliminar lista de reproducción", + "Delete playlist `x`?": "¿Borrar la lista de reproducción `x`?", + "Delete playlist": "Borrar lista de reproducción", "Create playlist": "Crear lista de reproducción", "Title": "Título", "Playlist privacy": "Privacidad de la lista de reproducción", @@ -333,4 +333,4 @@ "Playlists": "Listas de reproducción", "Community": "Comunidad", "Current version: ": "Versión actual: " -} \ No newline at end of file +} diff --git a/locales/fa.json b/locales/fa.json new file mode 100644 index 00000000..0f0900a9 --- /dev/null +++ b/locales/fa.json @@ -0,0 +1,353 @@ +{ + "`x` subscribers.([^.,0-9]|^)1([^.,0-9]|$)": "`x` مشترکان.([^.,0-9]|^)1([^.,0-9]|$)", + "`x` subscribers.": "`x` مشترکان.", + "`x` videos.([^.,0-9]|^)1([^.,0-9]|$)": "`x` ویدیو ها.([^.,0-9]|^)1([^.,0-9]|$)", + "`x` videos.": "`x` ویدیو ها.", + "`x` playlists.([^.,0-9]|^)1([^.,0-9]|$)": "`x` لیست های پخش.([^.,0-9]|^)1([^.,0-9]|$)", + "`x` playlists.": "`x` لیست های پخش.", + "LIVE": "زنده", + "Shared `x` ago": "به اشتراک گذاشته شده `x` پیش", + "Unsubscribe": "لغو اشتراک", + "Subscribe": "مشترک شدن", + "View channel on YouTube": "نمایش کانال در یوتیوب", + "View playlist on YouTube": "نمایش لیست پخش در یوتیوب", + "newest": "جدید تر", + "oldest": "قدیمی تر", + "popular": "محبوب", + "last": "آخرین", + "Next page": "صفحه بعد", + "Previous page": "صفحه قبل", + "Clear watch history?": "پاک کردن تاریخچه نمایش؟", + "New password": "گذرواژه جدید", + "New passwords must match": "گذارواژه های جدید باید باهم همخوانی داشته باشند", + "Cannot change password for Google accounts": "نمیتوان گذرواژه را برای حساب های کاربری گوگل تغییر داد", + "Authorize token?": "توکن دسترسی؟", + "Authorize token for `x`?": "توکن دسترسی برای `x`؟", + "Yes": "بله", + "No": "خیر", + "Import and Export Data": "وارد کردن و خارج کردن داده ها", + "Import": "وارد کردن", + "Import Invidious data": "وارد کردن داده Invidious", + "Import YouTube subscriptions": "وارد کردن اشتراک های یوتیوب", + "Import FreeTube subscriptions (.db)": "وارد کردن اشتراک های فری توب (.db)", + "Import NewPipe subscriptions (.json)": "وارد کردن اشتراک های نیو پایپ (.json)", + "Import NewPipe data (.zip)": "وارد کردن داده نیو پایپ (.zip)", + "Export": "خارج کردن", + "Export subscriptions as OPML": "خارج کردن اشتراک ها به عنوان OPML", + "Export subscriptions as OPML (for NewPipe & FreeTube)": "خارج کردن اشتراک ها به عنوان OPML (برای فری توب و نیو پایپ)", + "Export data as JSON": "خارج کردن داده ها به عنوان JSON", + "Delete account?": "حذف حساب کاربری؟", + "History": "تاریخچه", + "An alternative front-end to YouTube": "یک فرانت-اند جایگذین برای یوتیوب", + "JavaScript license information": "اطلاعات مجوز جاوا اسکریپت", + "source": "منبع", + "Log in": "ورود", + "Log in/register": "ورود/ثبت نام", + "Log in with Google": "ورود با گوگل", + "User ID": "شناسه کاربری", + "Password": "گذرواژه", + "Time (h:mm:ss):": "زمان (h:mm:ss):", + "Text CAPTCHA": "متن CAPTCHA", + "Image CAPTCHA": "تصویر CAPTCHA", + "Sign In": "ورود", + "Register": "ثبت نام", + "E-mail": "ایمیل", + "Google verification code": "کد تایید گوگل", + "Preferences": "ترجیحات", + "Player preferences": "ترجیحات نمایش‌دهنده", + "Always loop: ": "همیشه تکرار شنوده: ", + "Autoplay: ": "نمایش خودکار: ", + "Play next by default: ": "پخش بعدی به طور پیشفرض: ", + "Autoplay next video: ": "پخش خودکار ویدیو بعدی: ", + "Listen by default: ": "گوش کردن به طور پیشفرض: ", + "Proxy videos: ": "پروکسی ویدیو ها: ", + "Default speed: ": "سرعت پیشفرض: ", + "Preferred video quality: ": "کیفیت ویدیوی ترجیحی: ", + "Player volume: ": "صدای پخش کننده: ", + "Default comments: ": "نظرات پیشفرض: ", + "youtube": "یوتیوب", + "reddit": "ردیت", + "Default captions: ": "زیرنویس های پیشفرض: ", + "Fallback captions: ": "عقب گرد زیرنویس ها: ", + "Show related videos: ": "نمایش ویدیو های مرتبط: ", + "Show annotations by default: ": "نمایش حاشیه نویسی ها به طور پیشفرض: ", + "Visual preferences": "ترجیحات بصری", + "Player style: ": "حالت پخش کننده: ", + "Dark mode: ": "حالت تاریک: ", + "Theme: ": "تم: ", + "dark": "تاریک", + "light": "روشن", + "Thin mode: ": "حالت نازک: ", + "Subscription preferences": "ترجیحات اشتراک", + "Show annotations by default for subscribed channels: ": "نمایش حاشیه نویسی ها به طور پیشفرض برای کانال های مشترک شده: ", + "Redirect homepage to feed: ": "تغییر مسیر صفحه خانه به خوراک: ", + "Number of videos shown in feed: ": "تعداد ویدیو های نمایش داده شده در خوراک: ", + "Sort videos by: ": "مرتب سازی ویدیو ها بر اساس: ", + "published": "منتشر شده", + "published - reverse": "منتشر شده - معکوس", + "alphabetically": "بر اساس حروف الفبا", + "alphabetically - reverse": "بر اساس حروف الفبا - معکوس", + "channel name": "نام کانال", + "channel name - reverse": "نام کانال - معکوس", + "Only show latest video from channel: ": "تنها نمایش آخرین ویدیو های کانال: ", + "Only show latest unwatched video from channel: ": "تنها نمایش آخرین ویدیو های تماشا نشده از کانال: ", + "Only show unwatched: ": "تنها نمایش ویدیو های تماشا نشده: ", + "Only show notifications (if there are any): ": "تنها نمایش اعلان ها (اگر وجود داشته باشد) ", + "Enable web notifications": "فعال کردن اعلان های وب", + "`x` uploaded a video": "`x` یک ویدیو بارگذاری کرد", + "`x` is live": "`x` زنده است", + "Data preferences": "ترجیحات داده", + "Clear watch history": "پاک‌کردن تاریخچه تماشا", + "Import/export data": "وارد کردن/خارج کردن داده", + "Change password": "تغییر گذرواژه", + "Manage subscriptions": "مدیریت اشتراک ها", + "Manage tokens": "مدیریت توکن ها", + "Watch history": "تاریخچه تماشا", + "Delete account": "حذف حساب کاربری", + "Administrator preferences": "ترجیحات مدیریت", + "Default homepage: ": "صفحه خانه پیشفرض ", + "Feed menu: ": "منو خوراک: ", + "Top enabled: ": "بالا فعال شده: ", + "CAPTCHA enabled: ": "CAPTCHA فعال شده: ", + "Login enabled: ": "ورود فعال شده: ", + "Registration enabled: ": "ثبت نام فعال شده: ", + "Report statistics: ": "گذارش آمار: ", + "Save preferences": "ذخیره ترجیحات", + "Subscription manager": "مدیریت اشتراک", + "Token manager": "مدیر توکن", + "Token": "توکن", + "`x` subscriptions.([^.,0-9]|^)1([^.,0-9]|$)": "`x` اشتراک ها.([^.,0-9]|^)1([^.,0-9]|$)", + "`x` subscriptions.": "`x` اشتراک ها.", + "`x` tokens.([^.,0-9]|^)1([^.,0-9]|$)": "`x` توکن ها.([^.,0-9]|^)1([^.,0-9]|$)", + "`x` tokens.": "`x` توکن ها.", + "Import/export": "وارد کردن/خارج کردن", + "unsubscribe": "لغو اشتراک", + "revoke": "ابطال", + "Subscriptions": "اشتراک ها", + "`x` unseen notifications.([^.,0-9]|^)1([^.,0-9]|$)": "`x` اعلان نادیده.([^.,0-9]|^)1([^.,0-9]|$)", + "`x` unseen notifications.": "`x` اعلان نادیده.", + "search": "جستجو", + "Log out": "خروج", + "Released under the AGPLv3 by Omar Roth.": "منتشر شده تحت مجوز AGPLv3 توسط Omar Roth.", + "Source available here.": "منبع اینجا دردسترس است.", + "View JavaScript license information.": "نمایش اطلاعات مجوز جاوا اسکریپت.", + "View privacy policy.": "نمایش سیاست حفظ حریم خصوصی.", + "Trending": "روند", + "Public": "عمومی", + "Unlisted": "لیست نشده", + "Private": "خصوصی", + "View all playlists": "نمایش همه لیست پخش", + "Updated `x` ago": "بروز شده `x` پیش", + "Delete playlist `x`?": "حذف لیست پخش `x`؟", + "Delete playlist": "حذف لیست پخش", + "Create playlist": "ایجاد لیست پخش", + "Title": "عنوان", + "Playlist privacy": "حریم خصوصی لیست پخش", + "Editing playlist `x`": "تغییر لیست پخش `x`", + "Watch on YouTube": "تماشا در یوتیوب", + "Hide annotations": "مخفی کردن حاشیه نویسی ها", + "Show annotations": "نمایش حاشیه نویسی ها", + "Genre: ": "ژانر: ", + "License: ": "مجوز: ", + "Family friendly? ": "خانواده دوستانه؟ ", + "Wilson score: ": "امتیاز ویلسون: ", + "Engagement: ": "نامزدی: ", + "Whitelisted regions: ": "مناطق لیست سفید: ", + "Blacklisted regions: ": "مناطق لیست سیاه: ", + "Shared `x`": "به اشتراک گذاشته شده `x`", + "`x` views.([^.,0-9]|^)1([^.,0-9]|$)": "`x` بازدید.([^.,0-9]|^)1([^.,0-9]|$)", + "`x` views.": "`x` بازدید.", + "Premieres in `x`": "برای اولین بار در `x`", + "Premieres `x`": "برای اولین بار `x`", + "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "سلام! مثل اینکه تو جاوا اسکریپت رو خاموش کرده ای. اینجا کلیک کن تا نظرات را ببینی، این رو یادت باشه که ممکنه بارگذاری اونها کمی طول بکشه.", + "View YouTube comments": "نمایش نظرات یوتیوب", + "View more comments on Reddit": "نمایش نظرات بیشتر در ردیت", + "View `x` comments.([^.,0-9]|^)1([^.,0-9]|$)": "نمایش `x` نظرات.([^.,0-9]|^)1([^.,0-9]|$)", + "View `x` comments.": "نمایش `x` نظرات.", + "View Reddit comments": "نمایش نظرات ردیت", + "Hide replies": "مخفی کردن پاسخ ها", + "Show replies": "نمایش پاسخ ها", + "Incorrect password": "گذرواژه نا درست", + "Quota exceeded, try again in a few hours": "سهمیه بیشتر شده است، چند ساعت بعد دوباره تلاش کنید", + "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "قادر به ورود نیستید، مطمئن شوید احراز تایید-دو‌مرحله (Authenticator یا پیام‌کوتاه) خاموش باشد.", + "Invalid TFA code": "کد TFA نادرست است", + "Login failed. This may be because two-factor authentication is not turned on for your account.": "ورود با خطا مواجه شد. این ممکن است به خاطر احراز تایید-دو‌مرحله باشد که برای حساب کاربری شما فعال نشده است.", + "Wrong answer": "پاسخ غلط", + "Erroneous CAPTCHA": "CAPTCHA نا درست", + "CAPTCHA is a required field": "CAPTCHA یک فیلد ضروری است", + "User ID is a required field": "شناسه کاربری یک فیلد ضروری است", + "Password is a required field": "گذرواژه یک فیلد ضروری است", + "Wrong username or password": "نام کاربری یا گذرواژه غلط است", + "Please sign in using 'Log in with Google'": "لطفا با استفاده از 'ورود توسط گوگل' وارد شوید", + "Password cannot be empty": "گذرواژه نمیتواند خالی باشد", + "Password cannot be longer than 55 characters": "گذر واژه نمیتواند از ۵۵ کاراکتر بیشتر باشد", + "Please log in": "لطفا وارد شوید", + "Invidious Private Feed for `x`": "خوراک خصوصی زشت برای `x`", + "channel:`x`": "کانال: `x`", + "Deleted or invalid channel": "کانال نا معتبر یا پاک شده است", + "This channel does not exist.": "این کانال وجود ندارد.", + "Could not get channel info.": "نمیتوان اطلاعات کانال را دریافت کرد.", + "Could not fetch comments": "نمیتوان نظرات را دریافت کرد", + "View `x` replies.([^.,0-9]|^)1([^.,0-9]|$)": "نمایش `x` پاسخ ها.([^.,0-9]|^)1([^.,0-9]|$)", + "View `x` replies.": "نمایش `x` پاسخ ها.", + "`x` ago": "`x` پیش", + "Load more": "بارگذاری بیشتر", + "`x` points.([^.,0-9]|^)1([^.,0-9]|$)": "`x` نقطه ها.([^.,0-9]|^)1([^.,0-9]|$)", + "`x` points.": "`x` نقطه ها.", + "Could not create mix.": "نمیتوان میکس ساخت.", + "Empty playlist": "لیست پخش خالی", + "Not a playlist.": "یک لیست پخش نیست.", + "Playlist does not exist.": "لیست پخش وجود ندارد.", + "Could not pull trending pages.": "نمیتوان صفحه های پر طرفدار را بکشد.", + "Hidden field \"challenge\" is a required field": "فیلد مخفی \"چالش\" یک فیلد ضروری است", + "Hidden field \"token\" is a required field": "فیلد مخفی \"توکن\" یک فیلد ضروری است", + "Erroneous challenge": "چالش غلط", + "Erroneous token": "توکن غلط", + "No such user": "چنین کاربری وجود ندارد", + "Token is expired, please try again": "توکن ضروری است، لطفا دوباره تلاش کنید", + "English": "انگلیسی", + "English (auto-generated)": "انگلیسی (خودکار-تولید‌شده)", + "Afrikaans": "آفریکانس", + "Albanian": "آلبانیایی", + "Amharic": "امهری", + "Arabic": "عربی", + "Armenian": "ارمنی", + "Azerbaijani": "آذربایجانی", + "Bangla": "بنگالی", + "Basque": "باسکی", + "Belarusian": "بلاروسی", + "Bosnian": "بوسنیایی", + "Bulgarian": "بلغاری", + "Burmese": "برمه‌ای", + "Catalan": "کاتالان", + "Cebuano": "سبوانو", + "Chinese (Simplified)": "چینی (ساده شده)", + "Chinese (Traditional)": "چینی (سنتی)", + "Corsican": "کرس", + "Croatian": "کرواسی", + "Czech": "چکی", + "Danish": "دانمارکی", + "Dutch": "هلندی", + "Esperanto": "اسپرانتو", + "Estonian": "استونیایی", + "Filipino": "فلیپینی", + "Finnish": "فنلاندی", + "French": "فرانسوی", + "Galician": "گالیسی", + "Georgian": "گرجی", + "German": "آلمانی", + "Greek": "یونانی", + "Gujarati": "گجراتی", + "Haitian Creole": "کریول آییسینی", + "Hausa": "هوسه", + "Hawaiian": "هاوائی", + "Hebrew": "عبری", + "Hindi": "هندی", + "Hmong": "همونگ", + "Hungarian": "مجاری", + "Icelandic": "ایسلندی", + "Igbo": "ایگبو", + "Indonesian": "اندونزیایی", + "Irish": "شلتا", + "Italian": "ایتالیایی", + "Japanese": "ژاپنی", + "Javanese": "جاوه‌ای", + "Kannada": "کانارا", + "Kazakh": "قزاقی", + "Khmer": "خمر", + "Korean": "کره‌ای", + "Kurdish": "کردی", + "Kyrgyz": "قرقیزی", + "Lao": "لائو", + "Latin": "لاتین", + "Latvian": "لتونیایی", + "Lithuanian": "لیتوانیایی", + "Luxembourgish": "لوکزامبورگی", + "Macedonian": "مقدونی", + "Malagasy": "مالاگاسی", + "Malay": "مالایی", + "Malayalam": "مالایالم", + "Maltese": "مالتی", + "Maori": "مائوری", + "Marathi": "مراتی", + "Mongolian": "مغولی", + "Nepali": "نپالی", + "Norwegian Bokmål": "بوکمل", + "Nyanja": "چوایی", + "Pashto": "پشتو", + "Persian": "فارسی", + "Polish": "لهستانی", + "Portuguese": "پرتغالی", + "Punjabi": "پنجابی", + "Romanian": "رومانیایی", + "Russian": "روسی", + "Samoan": "ساموآیی", + "Scottish Gaelic": "گیلیک اسکاتلندی", + "Serbian": "صربی", + "Shona": "شونا", + "Sindhi": "سندی", + "Sinhala": "سینهالی", + "Slovak": "اسلواکی", + "Slovenian": "اسلونیایی", + "Somali": "سومالیایی", + "Southern Sotho": "سوتو", + "Spanish": "اسپانیایی", + "Spanish (Latin America)": "اسپانیایی (آمریکای لاتین)", + "Sundanese": "سوندایی", + "Swahili": "سواحلی", + "Swedish": "سوئدی", + "Tajik": "تاجیک", + "Tamil": "تامیلی", + "Telugu": "تلوگو", + "Thai": "تای", + "Turkish": "ترکی", + "Ukrainian": "اوکراینی", + "Urdu": "اردو", + "Uzbek": "ازبکی", + "Vietnamese": "ویتنامی", + "Welsh": "ولزی", + "Western Frisian": "فریسی غربی", + "Xhosa": "خوسایی", + "Yiddish": "ییدیش", + "Yoruba": "یوروبایی", + "Zulu": "زولو", + "`x` years.([^.,0-9]|^)1([^.,0-9]|$)": "`x` سال.([^.,0-9]|^)1([^.,0-9]|$)", + "`x` years.": "`x` سال.", + "`x` months.([^.,0-9]|^)1([^.,0-9]|$)": "`x` ماه.([^.,0-9]|^)1([^.,0-9]|$)", + "`x` months.": "`x` ماه.", + "`x` weeks.([^.,0-9]|^)1([^.,0-9]|$)": "`x` هفته.([^.,0-9]|^)1([^.,0-9]|$)", + "`x` weeks.": "`x` هفته.", + "`x` days.([^.,0-9]|^)1([^.,0-9]|$)": "`x` روز.([^.,0-9]|^)1([^.,0-9]|$)", + "`x` days.": "`x` روز.", + "`x` hours.([^.,0-9]|^)1([^.,0-9]|$)": "`x` ساعت.([^.,0-9]|^)1([^.,0-9]|$)", + "`x` hours.": "`x` ساعت.", + "`x` minutes.([^.,0-9]|^)1([^.,0-9]|$)": "`x` دقیقه.([^.,0-9]|^)1([^.,0-9]|$)", + "`x` minutes.": "`x` دقیقه.", + "`x` seconds.([^.,0-9]|^)1([^.,0-9]|$)": "`x` ثانیه.([^.,0-9]|^)1([^.,0-9]|$)", + "`x` seconds.": "`x` ثانیه.", + "Fallback comments: ": "نظرات عقب گرد: ", + "Popular": "محبوب", + "Top": "بالا", + "About": "درباره", + "Rating: ": "رتبه دهی: ", + "Language: ": "زبان: ", + "View as playlist": "نمایش به عنوان لیست پخش", + "Default": "پیشفرض", + "Music": "موسیقی", + "Gaming": "بازی", + "News": "اخبار", + "Movies": "فیلم‌ها", + "Download": "بارگیری", + "Download as: ": "بارگیری به عنوان: ", + "%A %B %-d, %Y": "%A %B %-d، %Y", + "(edited)": "(ویرایش شده)", + "YouTube comment permalink": "پیوست ثابت نظرات یوتیوب", + "permalink": "پیوست ثابت", + "`x` marked it with a ❤": "`x` نشان گذاری شده با یک ❤", + "Audio mode": "حالت صدا", + "Video mode": "حالت ویدیو", + "Videos": "ویدیو ها", + "Playlists": "لیست های پخش", + "Community": "اجتماع", + "Current version: ": "نسخه فعلی: " +} diff --git a/locales/fi.json b/locales/fi.json new file mode 100644 index 00000000..7de46bf4 --- /dev/null +++ b/locales/fi.json @@ -0,0 +1,353 @@ +{ + "`x` subscribers.([^.,0-9]|^)1([^.,0-9]|$)": "`x` tilaaja", + "`x` subscribers.": "`x` tilaajaa", + "`x` videos.([^.,0-9]|^)1([^.,0-9]|$)": "`x` video", + "`x` videos.": "`x` videota", + "`x` playlists.([^.,0-9]|^)1([^.,0-9]|$)": "`x` soittolista.([^.,0-9]|^)1([^.,0-9]|$)", + "`x` playlists.": "`x` soittolistaa", + "LIVE": "SUORA", + "Shared `x` ago": "Jaettu `x` sitten", + "Unsubscribe": "Peruuta tilaus", + "Subscribe": "Tilaa", + "View channel on YouTube": "Näytä kanava YouTubessa", + "View playlist on YouTube": "Näytä soittolista YouTubessa", + "newest": "uusin", + "oldest": "vanhin", + "popular": "suosittu", + "last": "viimeisin", + "Next page": "Seuraava sivu", + "Previous page": "Edellinen sivu", + "Clear watch history?": "Tyhjennä katseluhistoria?", + "New password": "Uusi salasana", + "New passwords must match": "Uusien salasanojen täytyy täsmätä", + "Cannot change password for Google accounts": "Google-tilien salasanaa ei voi vaihtaa", + "Authorize token?": "Valuutetaanko tunnus?", + "Authorize token for `x`?": "Valtuutetaanko tunnus `x`:lle?", + "Yes": "Kyllä", + "No": "Ei", + "Import and Export Data": "Tuo ja vie tietoja", + "Import": "Tuo", + "Import Invidious data": "Vie Invidious-tietoja", + "Import YouTube subscriptions": "Tuo YouTube-tilaukset", + "Import FreeTube subscriptions (.db)": "Tuo FreeTube-tilaukset (.db)", + "Import NewPipe subscriptions (.json)": "Tuo NewPipe-tilaukset (.json)", + "Import NewPipe data (.zip)": "Tuo NewPipe data (.zip)", + "Export": "Vie", + "Export subscriptions as OPML": "Vie tilaukset muodossa OPML", + "Export subscriptions as OPML (for NewPipe & FreeTube)": "Vie tilaukset muodossa OPML (NewPipe ja FreeTube)", + "Export data as JSON": "Vie data muodossa JSON", + "Delete account?": "Poista tili?", + "History": "Historia", + "An alternative front-end to YouTube": "Vaihtoehtoinen käyttöliittymä YouTubelle", + "JavaScript license information": "JavaScript-käyttöoikeustiedot", + "source": "lähde", + "Log in": "Kirjaudu sisään", + "Log in/register": "Kirjaudu sisään / Rekisteröidy", + "Log in with Google": "Kirjaudu sisään Googlella", + "User ID": "Käyttäjätunnus", + "Password": "Salasana", + "Time (h:mm:ss):": "Aika (h:mm:ss):", + "Text CAPTCHA": "Teksti CAPTCHA", + "Image CAPTCHA": "Kuva CAPTCHA", + "Sign In": "Kirjaudu sisään", + "Register": "Rekisteröidy", + "E-mail": "Sähköposti", + "Google verification code": "Google-vahvistuskoodi", + "Preferences": "Asetukset", + "Player preferences": "Soittimen asetukset", + "Always loop: ": "Aina silmukka: ", + "Autoplay: ": "Automaattinen toisto: ", + "Play next by default: ": "Toista seuraava oletuksena: ", + "Autoplay next video: ": "Toista seuraava video automaattisesti: ", + "Listen by default: ": "Kuuntele oletuksena: ", + "Proxy videos: ": "Proxy videot: ", + "Default speed: ": "Oletusnopeus: ", + "Preferred video quality: ": "Ensisijainen videon laatu: ", + "Player volume: ": "Soittimen äänenvoimakkuus: ", + "Default comments: ": "Oletuskommentit: ", + "youtube": "YouTube", + "reddit": "Reddit", + "Default captions: ": "Tekstitykset: ", + "Fallback captions: ": "Toissijaiset tekstitykset: ", + "Show related videos: ": "Näytä aiheeseen liittyviä videoita: ", + "Show annotations by default: ": "Näytä huomautukset oletuksena: ", + "Visual preferences": "Visuaaliset asetukset", + "Player style: ": "Soittimen tyyli: ", + "Dark mode: ": "Tumma tila: ", + "Theme: ": "Teema: ", + "dark": "tumma", + "light": "vaalea", + "Thin mode: ": "Kapea tila ", + "Subscription preferences": "Tilausten asetukset", + "Show annotations by default for subscribed channels: ": "Näytä oletuksena tilattujen kanavien huomautukset: ", + "Redirect homepage to feed: ": "Uudelleenohjaa kotisivu syötteeseen: ", + "Number of videos shown in feed: ": "Syötteessä näytettävien videoiden määrä: ", + "Sort videos by: ": "Videoiden lajitteluperuste: ", + "published": "julkaistu", + "published - reverse": "julkaistu - käänteinen", + "alphabetically": "aakkosjärjestys", + "alphabetically - reverse": "aakkosjärjestys - käänteinen", + "channel name": "kanavan nimi", + "channel name - reverse": "kanavan nimi - käänteinen", + "Only show latest video from channel: ": "Näytä vain uusin video kanavalta: ", + "Only show latest unwatched video from channel: ": "Näytä vain uusin katsomaton video kanavalta: ", + "Only show unwatched: ": "Näytä vain katsomattomat: ", + "Only show notifications (if there are any): ": "Näytä vain ilmoitukset (jos niitä on): ", + "Enable web notifications": "Näytä verkkoilmoitukset", + "`x` uploaded a video": "`x` latasi videon", + "`x` is live": "`x` lähettää suorana", + "Data preferences": "Tietojen asetukset", + "Clear watch history": "Tyhjennä katseluhistoria", + "Import/export data": "Tuo/vie tiedot", + "Change password": "Vaihda salasana", + "Manage subscriptions": "Hallinnoi tilauksia", + "Manage tokens": "Hallinnoi tunnuksia", + "Watch history": "Katseluhistoria", + "Delete account": "Poista tili", + "Administrator preferences": "Järjestelmänvalvojan asetukset", + "Default homepage: ": "Oletuskotisivu: ", + "Feed menu: ": "Syötevalikko: ", + "Top enabled: ": "Yläosa käytössä: ", + "CAPTCHA enabled: ": "CAPTCHA käytössä: ", + "Login enabled: ": "Kirjautuminen käytössä: ", + "Registration enabled: ": "Rekisteröityminen käytössä: ", + "Report statistics: ": "Raportoi tilastot: ", + "Save preferences": "Tallenna asetukset", + "Subscription manager": "Tilausten hallinnoija", + "Token manager": "Tunnusten hallinnoija", + "Token": "Tunnus", + "`x` subscriptions.([^.,0-9]|^)1([^.,0-9]|$)": "`x` tilausta.([^.,0-9]|^)1([^.,0-9]|$)", + "`x` subscriptions.": "`x` tilausta.", + "`x` tokens.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` tokens.": "", + "Import/export": "Tuo/vie", + "unsubscribe": "peru tilaus", + "revoke": "kumoa", + "Subscriptions": "Tilaukset", + "`x` unseen notifications.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` unseen notifications.": "", + "search": "", + "Log out": "", + "Released under the AGPLv3 by Omar Roth.": "", + "Source available here.": "", + "View JavaScript license information.": "", + "View privacy policy.": "", + "Trending": "", + "Public": "", + "Unlisted": "", + "Private": "", + "View all playlists": "", + "Updated `x` ago": "", + "Delete playlist `x`?": "", + "Delete playlist": "", + "Create playlist": "", + "Title": "", + "Playlist privacy": "", + "Editing playlist `x`": "", + "Watch on YouTube": "", + "Hide annotations": "", + "Show annotations": "", + "Genre: ": "", + "License: ": "", + "Family friendly? ": "", + "Wilson score: ": "", + "Engagement: ": "", + "Whitelisted regions: ": "", + "Blacklisted regions: ": "", + "Shared `x`": "", + "`x` views.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` views.": "", + "Premieres in `x`": "", + "Premieres `x`": "", + "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "", + "View YouTube comments": "", + "View more comments on Reddit": "", + "View `x` comments.([^.,0-9]|^)1([^.,0-9]|$)": "", + "View `x` comments.": "", + "View Reddit comments": "", + "Hide replies": "", + "Show replies": "", + "Incorrect password": "", + "Quota exceeded, try again in a few hours": "", + "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "", + "Invalid TFA code": "", + "Login failed. This may be because two-factor authentication is not turned on for your account.": "", + "Wrong answer": "", + "Erroneous CAPTCHA": "", + "CAPTCHA is a required field": "", + "User ID is a required field": "", + "Password is a required field": "", + "Wrong username or password": "", + "Please sign in using 'Log in with Google'": "", + "Password cannot be empty": "", + "Password cannot be longer than 55 characters": "", + "Please log in": "", + "Invidious Private Feed for `x`": "", + "channel:`x`": "", + "Deleted or invalid channel": "", + "This channel does not exist.": "", + "Could not get channel info.": "", + "Could not fetch comments": "", + "View `x` replies.([^.,0-9]|^)1([^.,0-9]|$)": "", + "View `x` replies.": "", + "`x` ago": "", + "Load more": "", + "`x` points.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` points.": "", + "Could not create mix.": "", + "Empty playlist": "", + "Not a playlist.": "", + "Playlist does not exist.": "", + "Could not pull trending pages.": "", + "Hidden field \"challenge\" is a required field": "", + "Hidden field \"token\" is a required field": "", + "Erroneous challenge": "", + "Erroneous token": "", + "No such user": "", + "Token is expired, please try again": "", + "English": "", + "English (auto-generated)": "", + "Afrikaans": "", + "Albanian": "", + "Amharic": "", + "Arabic": "", + "Armenian": "", + "Azerbaijani": "", + "Bangla": "", + "Basque": "", + "Belarusian": "", + "Bosnian": "", + "Bulgarian": "", + "Burmese": "", + "Catalan": "", + "Cebuano": "", + "Chinese (Simplified)": "", + "Chinese (Traditional)": "", + "Corsican": "", + "Croatian": "", + "Czech": "", + "Danish": "", + "Dutch": "", + "Esperanto": "", + "Estonian": "", + "Filipino": "", + "Finnish": "", + "French": "", + "Galician": "", + "Georgian": "", + "German": "", + "Greek": "", + "Gujarati": "", + "Haitian Creole": "", + "Hausa": "", + "Hawaiian": "", + "Hebrew": "", + "Hindi": "", + "Hmong": "", + "Hungarian": "", + "Icelandic": "", + "Igbo": "", + "Indonesian": "", + "Irish": "", + "Italian": "", + "Japanese": "", + "Javanese": "", + "Kannada": "", + "Kazakh": "", + "Khmer": "", + "Korean": "", + "Kurdish": "", + "Kyrgyz": "", + "Lao": "", + "Latin": "", + "Latvian": "", + "Lithuanian": "", + "Luxembourgish": "", + "Macedonian": "", + "Malagasy": "", + "Malay": "", + "Malayalam": "", + "Maltese": "", + "Maori": "", + "Marathi": "", + "Mongolian": "", + "Nepali": "", + "Norwegian Bokmål": "", + "Nyanja": "", + "Pashto": "", + "Persian": "", + "Polish": "", + "Portuguese": "", + "Punjabi": "", + "Romanian": "", + "Russian": "", + "Samoan": "", + "Scottish Gaelic": "", + "Serbian": "", + "Shona": "", + "Sindhi": "", + "Sinhala": "", + "Slovak": "", + "Slovenian": "", + "Somali": "", + "Southern Sotho": "", + "Spanish": "", + "Spanish (Latin America)": "", + "Sundanese": "", + "Swahili": "", + "Swedish": "", + "Tajik": "", + "Tamil": "", + "Telugu": "", + "Thai": "", + "Turkish": "", + "Ukrainian": "", + "Urdu": "", + "Uzbek": "", + "Vietnamese": "", + "Welsh": "", + "Western Frisian": "", + "Xhosa": "", + "Yiddish": "", + "Yoruba": "", + "Zulu": "", + "`x` years.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` years.": "", + "`x` months.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` months.": "", + "`x` weeks.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` weeks.": "", + "`x` days.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` days.": "", + "`x` hours.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` hours.": "", + "`x` minutes.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` minutes.": "", + "`x` seconds.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` seconds.": "", + "Fallback comments: ": "", + "Popular": "", + "Top": "", + "About": "", + "Rating: ": "", + "Language: ": "", + "View as playlist": "", + "Default": "", + "Music": "", + "Gaming": "", + "News": "", + "Movies": "", + "Download": "", + "Download as: ": "", + "%A %B %-d, %Y": "", + "(edited)": "", + "YouTube comment permalink": "", + "permalink": "", + "`x` marked it with a ❤": "", + "Audio mode": "", + "Video mode": "", + "Videos": "", + "Playlists": "", + "Community": "", + "Current version: ": "" +} diff --git a/locales/fr.json b/locales/fr.json index 24cabdea..664e25f5 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -16,7 +16,7 @@ "Previous page": "Page précédente", "Clear watch history?": "Êtes-vous sûr de vouloir supprimer l'historique des vidéos regardées ?", "New password": "Nouveau mot de passe", - "New passwords must match": "Les champs \"Nouveau mot de passe\" doivent être identiques", + "New passwords must match": "Les nouveaux mots de passe doivent correspondre", "Cannot change password for Google accounts": "Le mot de passe d'un compte Google ne peut pas être changé depuis Invidious", "Authorize token?": "Autoriser le token ?", "Authorize token for `x`?": "Autoriser le token pour `x` ?", @@ -48,7 +48,7 @@ "Image CAPTCHA": "CAPTCHA Image", "Sign In": "Se connecter", "Register": "S'inscrire", - "E-mail": "E-mail", + "E-mail": "Courriel", "Google verification code": "Code de vérification Google", "Preferences": "Préférences", "Player preferences": "Préférences du lecteur", @@ -106,7 +106,7 @@ "Feed menu: ": "Préferences des abonnements : ", "Top enabled: ": "Top activé : ", "CAPTCHA enabled: ": "CAPTCHA activé : ", - "Login enabled: ": "Connexion activé : ", + "Login enabled: ": "Connexion activée : ", "Registration enabled: ": "Inscription activée : ", "Report statistics: ": "Télémétrie activé : ", "Save preferences": "Enregistrer les préférences", @@ -145,7 +145,7 @@ "License: ": "Licence : ", "Family friendly? ": "Vidéo tout public ? ", "Wilson score: ": "Score de Wilson : ", - "Engagement: ": "Pourcentage de spectateur aillant appuyé sur \"J'aime\" ou \"J'aime Pas\" : ", + "Engagement: ": "Taux d'implication : ", "Whitelisted regions: ": "Régions sur liste blanche : ", "Blacklisted regions: ": "Régions sur liste noire : ", "Shared `x`": "Ajoutée le `x`", @@ -170,12 +170,12 @@ "User ID is a required field": "Veuillez entrer un Identifiant Utilisateur", "Password is a required field": "Veuillez entrer un Mot de passe", "Wrong username or password": "Nom d'utilisateur ou mot de passe invalide", - "Please sign in using 'Log in with Google'": "Veuillez vous connecter en utilisant \"Se connecter avec Google\"", + "Please sign in using 'Log in with Google'": "Veuillez vous connecter en utilisant « Se connecter avec Google »", "Password cannot be empty": "Le mot de passe ne peut pas être vide", "Password cannot be longer than 55 characters": "Le mot de passe ne doit pas comporter plus de 55 caractères", "Please log in": "Veuillez vous connecter", "Invidious Private Feed for `x`": "Flux RSS privé pour `x`", - "channel:`x`": "chaîne:`x`", + "channel:`x`": "chaîne :`x`", "Deleted or invalid channel": "Chaîne supprimée ou invalide", "This channel does not exist.": "Cette chaine n'existe pas.", "Could not get channel info.": "Impossible de charger les informations de cette chaîne.", @@ -189,8 +189,8 @@ "Not a playlist.": "La liste de lecture est invalide.", "Playlist does not exist.": "La liste de lecture n'existe pas.", "Could not pull trending pages.": "Impossible de charger les pages de tendances.", - "Hidden field \"challenge\" is a required field": "Le champ masqué \"challenge\" est un champ obligatoire", - "Hidden field \"token\" is a required field": "Le champ caché \"token\" est requis", + "Hidden field \"challenge\" is a required field": "Le champ masqué « challenge » est un champ obligatoire", + "Hidden field \"token\" is a required field": "Le champ caché « token » est requis", "Erroneous challenge": "Challenge invalide", "Erroneous token": "Token invalide", "No such user": "Cet utilisateur n'existe pas", @@ -217,21 +217,21 @@ "Croatian": "Croate", "Czech": "Tchèque", "Danish": "Danois", - "Dutch": "Hollandais", + "Dutch": "Néerlandais", "Esperanto": "Espéranto", "Estonian": "Estonien", "Filipino": "Philippin", - "Finnish": "Finlandais", + "Finnish": "Finnois", "French": "Français", "Galician": "Galicien", "Georgian": "Géorgien", "German": "Allemand", "Greek": "Grec", "Gujarati": "Gujarati", - "Haitian Creole": "Créole Haïtien", + "Haitian Creole": "Créole haïtien", "Hausa": "Haoussa", "Hawaiian": "Hawaïen", - "Hebrew": "Hébraïque", + "Hebrew": "Hébreu", "Hindi": "Hindi", "Hmong": "Hmong", "Hungarian": "Hongrois", @@ -262,21 +262,21 @@ "Marathi": "Marathi", "Mongolian": "Mongol", "Nepali": "Népalais", - "Norwegian Bokmål": "Norvégien", + "Norwegian Bokmål": "Norvégien bokmål", "Nyanja": "Nyanja", - "Pashto": "Pachtou", + "Pashto": "Pachto", "Persian": "Persan", "Polish": "Polonais", "Portuguese": "Portugais", - "Punjabi": "Punjabi", + "Punjabi": "Pendjabi", "Romanian": "Roumain", "Russian": "Russe", "Samoan": "Samoan", - "Scottish Gaelic": "Eaélique Ècossais", + "Scottish Gaelic": "Gaélique écossais", "Serbian": "Serbe", "Shona": "Shona", "Sindhi": "Sindhi", - "Sinhala": "Cinghalais", + "Sinhala": "Singhalais", "Slovak": "Slovaque", "Slovenian": "Slovène", "Somali": "Somalien", @@ -286,9 +286,9 @@ "Sundanese": "Sundanais", "Swahili": "Swahili", "Swedish": "Suédois", - "Tajik": "Tajik", + "Tajik": "Tadjik", "Tamil": "Tamil", - "Telugu": "Telugu", + "Telugu": "Télougou", "Thai": "Thaï", "Turkish": "Turc", "Ukrainian": "Ukrainien", @@ -317,7 +317,7 @@ "View as playlist": "Voir en tant que liste de lecture", "Default": "Défaut", "Music": "Musique", - "Gaming": "Jeux Vidéo", + "Gaming": "Jeux vidéo", "News": "Actualités", "Movies": "Films", "Download": "Télécharger", @@ -325,7 +325,7 @@ "%A %B %-d, %Y": "%A %-d %B %Y", "(edited)": "(modifié)", "YouTube comment permalink": "Lien permanent vers le commentaire sur YouTube", - "permalink": "Lien permanent", + "permalink": "permalien", "`x` marked it with a ❤": "`x` l'a marqué d'un ❤", "Audio mode": "Mode audio", "Video mode": "Mode vidéo", @@ -333,4 +333,4 @@ "Playlists": "Listes de lecture", "Community": "Communauté", "Current version: ": "Version actuelle : " -} \ No newline at end of file +} diff --git a/locales/hr.json b/locales/hr.json new file mode 100644 index 00000000..a9a179f3 --- /dev/null +++ b/locales/hr.json @@ -0,0 +1,387 @@ +{ + "`x` subscribers": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` pretplatnika.([^.,0-9]|^)1([^.,0-9]|$)", + "": "`x` pretplatnika." + }, + "`x` videos": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` videa.([^.,0-9]|^)1([^.,0-9]|$)", + "": "`x` videa." + }, + "`x` playlists": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` playliste.([^.,0-9]|^)1([^.,0-9]|$)", + "": "`x` playliste." + }, + "LIVE": "UŽIVO", + "Shared `x` ago": "Dijeljeno prije `x`", + "Unsubscribe": "Odjavi pretplatu", + "Subscribe": "Pretplati se", + "View channel on YouTube": "Prikaži kanal na YouTubeu", + "View playlist on YouTube": "Prikaži playlistu na YouTubeu", + "newest": "najnovije", + "oldest": "najstarije", + "popular": "popularni", + "last": "zadnji", + "Next page": "Sljedeća stranica", + "Previous page": "Prethodna stranica", + "Clear watch history?": "Izbrisati povijest gledanja?", + "New password": "Nova lozinka", + "New passwords must match": "Nove lozinke se moraju poklapati", + "Cannot change password for Google accounts": "Nije moguće promijeniti lozinku za Google račune", + "Authorize token?": "Autorizirati token?", + "Authorize token for `x`?": "Autorizirati token za `x`?", + "Yes": "Da", + "No": "Ne", + "Import and Export Data": "Uvezi i izvezi podatke", + "Import": "Uvezi", + "Import Invidious data": "Uvezi Invidious podatke", + "Import YouTube subscriptions": "Uvezi YouTube pretplate", + "Import FreeTube subscriptions (.db)": "Uvezi FreeTube pretplate (.db)", + "Import NewPipe subscriptions (.json)": "Uvezi NewPipe pretplate (.json)", + "Import NewPipe data (.zip)": "Uvezi NewPipe podatke (.zip)", + "Export": "Izvezi", + "Export subscriptions as OPML": "Izvezi pretplate kao OPML", + "Export subscriptions as OPML (for NewPipe & FreeTube)": "Izvezi pretplate kao OPML (za NewPipe i FreeTube)", + "Export data as JSON": "Izvezi podatke kao JSON", + "Delete account?": "Izbrisati račun?", + "History": "Povijest", + "An alternative front-end to YouTube": "Alternativa za YouTube", + "JavaScript license information": "Informacije o JavaScript licenci", + "source": "izvor", + "Log in": "Prijavi se", + "Log in/register": "Prijavi se/registriraj se", + "Log in with Google": "Prijavi se pomoću Googlea", + "User ID": "Korisnički ID", + "Password": "Lozinka", + "Time (h:mm:ss):": "Vrijeme (h:mm:ss):", + "Text CAPTCHA": "Tekstualni CAPTCHA", + "Image CAPTCHA": "Slikovni CAPTCHA", + "Sign In": "Prijava", + "Register": "Registriraj se", + "E-mail": "E-mail", + "Google verification code": "Googleov potvrdni kod", + "Preferences": "Postavke", + "Player preferences": "Postavke playera", + "Always loop: ": "Uvijek ponavljaj: ", + "Autoplay: ": "Automatski reproduciraj: ", + "Play next by default: ": "Standardno reproduciraj sljedeći: ", + "Autoplay next video: ": "Automatski reproduciraj sljedeći video: ", + "Listen by default: ": "Standardno slušaj: ", + "Proxy videos: ": "Koristi posrednika videa: ", + "Default speed: ": "Standardna brzina: ", + "Preferred video quality: ": "Primarna kvaliteta videa: ", + "Player volume: ": "Glasnoća playera: ", + "Default comments: ": "Standardni komentari: ", + "youtube": "youtube", + "reddit": "reddit", + "Default captions: ": "Standardni titlovi: ", + "Fallback captions: ": "Alternativni titlovi: ", + "Show related videos: ": "Prikaži povezana videa: ", + "Show annotations by default: ": "Standardno prikaži napomene: ", + "Visual preferences": "Postavke prikaza", + "Player style: ": "Stil playera: ", + "Dark mode: ": "Tamni modus: ", + "Theme: ": "Tema: ", + "dark": "tamno", + "light": "svijetlo", + "Thin mode: ": "Pojednostavljen prikaz: ", + "Subscription preferences": "Postavke pretplata", + "Show annotations by default for subscribed channels: ": "Standardno prikaži napomene za pretplaćene kanale: ", + "Redirect homepage to feed: ": "Preusmjeri početnu stranicu na feed: ", + "Number of videos shown in feed: ": "Broj prikazanih videa u feedu: ", + "Sort videos by: ": "Razvrstaj videa prema: ", + "published": "objavljeno", + "published - reverse": "objavljeno – obrnuto", + "alphabetically": "abecednim redom", + "alphabetically - reverse": "abecednim redom – obrnuto", + "channel name": "ime kanala", + "channel name - reverse": "ime kanala – obrnuto", + "Only show latest video from channel: ": "Prikaži samo najnovija videa kanala: ", + "Only show latest unwatched video from channel: ": "Prikaži samo najnovija nepogledana videa kanala: ", + "Only show unwatched: ": "Prikaži samo nepogledane: ", + "Only show notifications (if there are any): ": "Prikaži samo obavijesti (ako ih ima): ", + "Enable web notifications": "Aktiviraj web-obavijesti", + "`x` uploaded a video": "`x` je poslao/la video", + "`x` is live": "`x` je uživo", + "Data preferences": "Postavke podataka", + "Clear watch history": "Izbriši povijest gledanja", + "Import/export data": "Uvezi/izvezi podatke", + "Change password": "Promijeni lozinku", + "Manage subscriptions": "Upravljaj pretplatama", + "Manage tokens": "Upravljaj tokenima", + "Watch history": "Povijest gledanja", + "Delete account": "Izbriši račun", + "Administrator preferences": "Postavke administratora", + "Default homepage: ": "Standardna početna stranica: ", + "Feed menu: ": "Izbornik za feedove: ", + "Top enabled: ": "Najbolji aktivirani: ", + "CAPTCHA enabled: ": "Aktivirani CAPTCHA: ", + "Login enabled: ": "Prijava aktivirana: ", + "Registration enabled: ": "Registracija aktivirana: ", + "Report statistics: ": "Izvještaj o statistici: ", + "Save preferences": "Spremi postavke", + "Subscription manager": "Upravljanje pretplatama", + "Token manager": "Upravljanje tokenima", + "Token": "Token", + "`x` subscriptions": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` pretplate.([^.,0-9]|^)1([^.,0-9]|$)", + "": "`x` pretplate." + }, + "`x` tokens": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` tokena.([^.,0-9]|^)1([^.,0-9]|$)", + "": "`x` tokena." + }, + "Import/export": "Uvezi/izvezi", + "unsubscribe": "odjavi pretplatu", + "revoke": "opozovi", + "Subscriptions": "Pretplate", + "`x` unseen notifications": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` neviđene obavijesti.([^.,0-9]|^)1([^.,0-9]|$)", + "": "`x` neviđene obavijesti." + }, + "search": "traži", + "Log out": "Odjavi se", + "Released under the AGPLv3 by Omar Roth.": "Izdano pod licencom AGPLv3, Omar Roth.", + "Source available here.": "Izvor je ovdje dostupan.", + "View JavaScript license information.": "Prikaži informacije o JavaScript licenci.", + "View privacy policy.": "Prikaži politiku privatnosti.", + "Trending": "U trendu", + "Public": "Javno", + "Unlisted": "Nenavedeno", + "Private": "Privatno", + "View all playlists": "Prikaži sve playliste", + "Updated `x` ago": "Aktualizirano prije `x`", + "Delete playlist `x`?": "Izbrisati playlistu `x`?", + "Delete playlist": "Izbriši playlistu", + "Create playlist": "Stvori playlistu", + "Title": "Naslov", + "Playlist privacy": "Privatnost playliste", + "Editing playlist `x`": "Uređivanje playliste `x`", + "Watch on YouTube": "Gledaj na YouTubeu", + "Hide annotations": "Sakrij napomene", + "Show annotations": "Prikaži napomene", + "Genre: ": "Žanr: ", + "License: ": "Licenca: ", + "Family friendly? ": "Pogodan za cijelu obitelj? ", + "Wilson score: ": "Wilson rezultat: ", + "Engagement: ": "Sudjelovanje: ", + "Whitelisted regions: ": "Odobrene regije: ", + "Blacklisted regions: ": "Blokirane regije: ", + "Shared `x`": "Dijeljeno `x`", + "`x` views": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` gledanja.([^.,0-9]|^)1([^.,0-9]|$)", + "": "`x` gledanja." + }, + "Premieres in `x`": "Premijera za `x`", + "Premieres `x`": "Premijera `x`", + "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Bok! Izgleda da je JavaScript isključen. Pritisni ovdje za prikaz komentara. Učitavanje će možda trajati malo duže.", + "View YouTube comments": "Prikaži YouTube komentare", + "View more comments on Reddit": "Prikaži još komentara na Redditu", + "View `x` comments": { + "([^.,0-9]|^)1([^.,0-9]|$)": "Prikaži `x` komentara.([^.,0-9]|^)1([^.,0-9]|$)", + "": "Prikaži `x` komentara." + }, + "View Reddit comments": "Prikaži Reddit komentare", + "Hide replies": "Sakrij odgovore", + "Show replies": "Prikaži odgovore", + "Incorrect password": "Neispravna lozinka", + "Quota exceeded, try again in a few hours": "Kvota je prekoračena. Pokušaj ponovo za par sati", + "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Prijava neuspjela. Provjeri da je dvofaktorska autentifikacija uključena (Authenticator ili SMS).", + "Invalid TFA code": "Neispravan TFA kod", + "Login failed. This may be because two-factor authentication is not turned on for your account.": "Prijava neuspjela. Možda zato što za tvoj račun nije uključena dvofaktorska autentifikacija.", + "Wrong answer": "Krivi odgovor", + "Erroneous CAPTCHA": "Neispravan CAPTCHA", + "CAPTCHA is a required field": "CAPTCHA je obavezno polje", + "User ID is a required field": "Korisnički ID je obavezno polje", + "Password is a required field": "Polje lozinke je obavezno polje", + "Wrong username or password": "Krivo korisničko ime ili lozinka", + "Please sign in using 'Log in with Google'": "Za prijavu koristi „Prijavi se pomoću Googlea”", + "Password cannot be empty": "Polje lozinke ne smije ostati prazno", + "Password cannot be longer than 55 characters": "Lozinka ne može biti duža od 55 znakova", + "Please log in": "Prijavi se", + "Invidious Private Feed for `x`": "Invidious privatni feed za `x`", + "channel:`x`": "kanal:`x`", + "Deleted or invalid channel": "Izbrisan ili neispravan kanal", + "This channel does not exist.": "Ovaj kanal ne postoji.", + "Could not get channel info.": "Neuspjelo dobivanje podataka kanala.", + "Could not fetch comments": "Neuspjelo dohvaćanje komentara", + "View `x` replies": { + "([^.,0-9]|^)1([^.,0-9]|$)": "Prikaži `x` odgovora.([^.,0-9]|^)1([^.,0-9]|$)", + "": "Prikaži `x` odgovora." + }, + "`x` ago": "prije `x`", + "Load more": "Učitaj više", + "`x` points": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` bodova.([^.,0-9]|^)1([^.,0-9]|$)", + "": "`x` bodova." + }, + "Could not create mix.": "Neuspjelo stvaranje miksa.", + "Empty playlist": "Prazna playlista", + "Not a playlist.": "Nije playlista.", + "Playlist does not exist.": "Playlista ne postoji.", + "Could not pull trending pages.": "Neuspjelo preuzimanje stranica u trendu.", + "Hidden field \"challenge\" is a required field": "Skriveno polje „izazov” je obavezno polje", + "Hidden field \"token\" is a required field": "Skriveno polje „token” je obavezno polje", + "Erroneous challenge": "Neispravan izazov", + "Erroneous token": "Neispravan token", + "No such user": "Takav korisnik ne postoji", + "Token is expired, please try again": "Token je istekao, pokušaj ponovo", + "English": "Engleski", + "English (auto-generated)": "Engleski (automatki generirano)", + "Afrikaans": "Afrikaanski", + "Albanian": "Albanski", + "Amharic": "Amharski", + "Arabic": "Arapski", + "Armenian": "Armenski", + "Azerbaijani": "Azerbajdžanski", + "Bangla": "Bengalski", + "Basque": "Baskijski", + "Belarusian": "Bjeloruski", + "Bosnian": "Bošnjački", + "Bulgarian": "Bugarski", + "Burmese": "Burmanski", + "Catalan": "Katalonski", + "Cebuano": "Cebuano", + "Chinese (Simplified)": "Kineski (pojednostavljeni)", + "Chinese (Traditional)": "Kineski (tradicionalni)", + "Corsican": "Korzikanski", + "Croatian": "Hrvatski", + "Czech": "Češki", + "Danish": "Danski", + "Dutch": "Nizozemski", + "Esperanto": "Esperanto", + "Estonian": "Estonski", + "Filipino": "Filipinski", + "Finnish": "Finski", + "French": "Francuski", + "Galician": "Galicijski", + "Georgian": "Gruzijski", + "German": "Njemački", + "Greek": "Grčki", + "Gujarati": "Gudžaratski", + "Haitian Creole": "Haitjanski kreolski", + "Hausa": "Hauski", + "Hawaiian": "Havajski", + "Hebrew": "Hebrejski", + "Hindi": "Hindski", + "Hmong": "Hmong", + "Hungarian": "Mađarski", + "Icelandic": "Islandski", + "Igbo": "Igboški", + "Indonesian": "Indonezijski", + "Irish": "Irski", + "Italian": "Talijanski", + "Japanese": "Japanski", + "Javanese": "Javanski", + "Kannada": "Kannada", + "Kazakh": "Kazaški", + "Khmer": "Kmerski", + "Korean": "Korejski", + "Kurdish": "Kurdski", + "Kyrgyz": "Kirgiški", + "Lao": "Laoški", + "Latin": "Latinski", + "Latvian": "Latvijski", + "Lithuanian": "Litvanski", + "Luxembourgish": "Luksemburgški", + "Macedonian": "Makedonski", + "Malagasy": "Malagaški", + "Malay": "Malajski", + "Malayalam": "Malajalamski", + "Maltese": "Malteški", + "Maori": "Maorski", + "Marathi": "Marathi", + "Mongolian": "Mongolski", + "Nepali": "Nepalski", + "Norwegian Bokmål": "Norveški Bokmål", + "Nyanja": "Nijanja", + "Pashto": "Paštunski", + "Persian": "Perzijski", + "Polish": "Poljski", + "Portuguese": "Portugalski", + "Punjabi": "Pandžapski", + "Romanian": "Rumunjski", + "Russian": "Ruski", + "Samoan": "Samoanski", + "Scottish Gaelic": "Škotski galski", + "Serbian": "Srpski", + "Shona": "Šona", + "Sindhi": "Sindhi", + "Sinhala": "Singaleški", + "Slovak": "Slovački", + "Slovenian": "Slovenski", + "Somali": "Somalijski", + "Southern Sotho": "Sjeverno samski", + "Spanish": "Španjolski", + "Spanish (Latin America)": "Španjolski (Latinska Amerika)", + "Sundanese": "Sundski", + "Swahili": "Svahili", + "Swedish": "Švedski", + "Tajik": "Tadžički", + "Tamil": "Tamilski", + "Telugu": "Teluški", + "Thai": "Tajlandski", + "Turkish": "Turski", + "Ukrainian": "Ukrajinski", + "Urdu": "Urdski", + "Uzbek": "Uzbečki", + "Vietnamese": "Vijetnamski", + "Welsh": "Velški", + "Western Frisian": "Zapadni frizijski", + "Xhosa": "Xhosa", + "Yiddish": "Jidiš", + "Yoruba": "Jorubški", + "Zulu": "Zulu", + "`x` years": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` g.([^.,0-9]|^)1([^.,0-9]|$)", + "": "`x` g." + }, + "`x` months": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` mj.([^.,0-9]|^)1([^.,0-9]|$)", + "": "`x` mj." + }, + "`x` weeks": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` tj.([^.,0-9]|^)1([^.,0-9]|$)", + "": "`x` tj." + }, + "`x` days": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` dana.([^.,0-9]|^)1([^.,0-9]|$)", + "": "`x` dana." + }, + "`x` hours": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` h.([^.,0-9]|^)1([^.,0-9]|$)", + "": "`x` h." + }, + "`x` minutes": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` min.([^.,0-9]|^)1([^.,0-9]|$)", + "": "`x` min." + }, + "`x` seconds": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` s.([^.,0-9]|^)1([^.,0-9]|$)", + "": "`x` s." + }, + "Fallback comments: ": "Alternativni komentari: ", + "Popular": "Popularni", + "Top": "Najbolji", + "About": "Informacije", + "Rating: ": "Ocjena: ", + "Language: ": "Jezik: ", + "View as playlist": "Prikaži kao playlistu", + "Default": "Standardno", + "Music": "Glazba", + "Gaming": "Videoigre", + "News": "Vijesti", + "Movies": "Filmovi", + "Download": "Preuzmi", + "Download as: ": "Preuzmi kao: ", + "%A %B %-d, %Y": "%A, %-d. %B %Y.", + "(edited)": "(uređeno)", + "YouTube comment permalink": "Permalink YouTube komentara", + "permalink": "permalink", + "`x` marked it with a ❤": "Označeno sa ❤ od `x`", + "Audio mode": "Audio modus", + "Video mode": "Videomodus", + "Videos": "Videa", + "Playlists": "Playliste", + "Community": "Zajednica", + "Current version: ": "Trenutačna verzija: " +} diff --git a/locales/id.json b/locales/id.json new file mode 100644 index 00000000..217ea1c7 --- /dev/null +++ b/locales/id.json @@ -0,0 +1,387 @@ +{ + "`x` subscribers": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` pelanggan.([^.,0-9]|^)1([^.,0-9]|$)", + "": "`x` pelanggan." + }, + "`x` videos": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` video.([^.,0-9]|^)1([^.,0-9]|$)", + "": "`x` video." + }, + "`x` playlists": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` daftar putar.([^.,0-9]|^)1([^.,0-9]|$)", + "": "`x` daftar putar." + }, + "LIVE": "SIARAN LANGSUNG", + "Shared `x` ago": "Dibagikan`x` lalu", + "Unsubscribe": "Batal Langganan", + "Subscribe": "Langganan", + "View channel on YouTube": "Lihat kanal di YouTube", + "View playlist on YouTube": "Lihat daftar putar di YouTube", + "newest": "terbaru", + "oldest": "terlawas", + "popular": "populer", + "last": "terakhir", + "Next page": "Halaman berikutnya", + "Previous page": "Halaman sebelumnya", + "Clear watch history?": "Bersihkan riwayat tontonan?", + "New password": "Kata sandi baru", + "New passwords must match": "Kata sandi baru harus cocok", + "Cannot change password for Google accounts": "Tidak dapat mengganti kata sandi untuk akun Google", + "Authorize token?": "Otorisasi token?", + "Authorize token for `x`?": "Otorisasi token untuk `x`?", + "Yes": "Ya", + "No": "Tidak", + "Import and Export Data": "Impor dan Ekspor Data", + "Import": "Impor", + "Import Invidious data": "Impor data Invidious", + "Import YouTube subscriptions": "Impor langganan YouTube", + "Import FreeTube subscriptions (.db)": "Impor langganan FreeTube (.db)", + "Import NewPipe subscriptions (.json)": "Impor langganan NewPipe (.json)", + "Import NewPipe data (.zip)": "Impor data NewPipe (.zip)", + "Export": "Ekspor", + "Export subscriptions as OPML": "Ekspor langganan sebagai OPML", + "Export subscriptions as OPML (for NewPipe & FreeTube)": "Ekspor langganan sebagai OPML (untuk NewPipe & FreeTube)", + "Export data as JSON": "Ekspor data sebagai JSON", + "Delete account?": "Hapus akun?", + "History": "Riwayat", + "An alternative front-end to YouTube": "Sebuah alternatif front-end untuk YouTube", + "JavaScript license information": "Informasi lisensi JavaScript", + "source": "sumber", + "Log in": "Masuk", + "Log in/register": "Daftar", + "Log in with Google": "Masuk dengan Google", + "User ID": "ID Pengguna", + "Password": "Kata Sandi", + "Time (h:mm:ss):": "Waktu (j:mm:dd):", + "Text CAPTCHA": "Teks CAPTCHA", + "Image CAPTCHA": "Gambar CAPTCHA", + "Sign In": "Masuk", + "Register": "Daftar", + "E-mail": "Surel", + "Google verification code": "Kode verifikasi Google", + "Preferences": "Preferensi", + "Player preferences": "Preferensi pemutar", + "Always loop: ": "Selalu ulangi: ", + "Autoplay: ": "Putar-Otomatis: ", + "Play next by default: ": "Putar selanjutnya secara default: ", + "Autoplay next video: ": "Otomatis-Putar video berikutnya: ", + "Listen by default: ": "Dengarkan secara default: ", + "Proxy videos: ": "Video Proksi: ", + "Default speed: ": "Kecepatan default: ", + "Preferred video quality: ": "Kualitas video yang disukai: ", + "Player volume: ": "Volume pemutar: ", + "Default comments: ": "Komentar default: ", + "youtube": "youtube", + "reddit": "reddit", + "Default captions: ": "Subtitel default: ", + "Fallback captions: ": "", + "Show related videos: ": "Tampilkan video terkait: ", + "Show annotations by default: ": "Tampilkan anotasi secara default: ", + "Visual preferences": "Preferensi visual", + "Player style: ": "Gaya pemutar: ", + "Dark mode: ": "Mode gelap: ", + "Theme: ": "Tema: ", + "dark": "gelap", + "light": "terang", + "Thin mode: ": "Mode tipis: ", + "Subscription preferences": "Preferensi langganan", + "Show annotations by default for subscribed channels: ": "Tampilkan anotasi secara default untuk kanal langganan: ", + "Redirect homepage to feed: ": "Arahkan kembali laman beranda ke umpan: ", + "Number of videos shown in feed: ": "Jumlah video ditampilkan di umpan: ", + "Sort videos by: ": "Urutkan video berdasarkan: ", + "published": "dipublikasi", + "published - reverse": "dipublikasi - sebaliknya", + "alphabetically": "menurut abjad", + "alphabetically - reverse": "menurut abjad - sebaliknya", + "channel name": "nama kanal", + "channel name - reverse": "nama kanal - sebaliknya", + "Only show latest video from channel: ": "Hanya tampilkan video terbaru dari kanal: ", + "Only show latest unwatched video from channel: ": "Hanya tampilkan video belum ditonton terbaru dari kanal: ", + "Only show unwatched: ": "Hanya tampilkan belum ditonton: ", + "Only show notifications (if there are any): ": "Hanya tampilkan pemberitahuan (jika ada): ", + "Enable web notifications": "Aktifkan pemberitahuan web", + "`x` uploaded a video": "`x` mengunggah video", + "`x` is live": "`x` sedang siaran langsung", + "Data preferences": "Preferensi Data", + "Clear watch history": "Bersihkan riwayat tontonan", + "Import/export data": "Impor/Ekspor data", + "Change password": "Ganti kata sandi", + "Manage subscriptions": "Atur langganan", + "Manage tokens": "Atur token", + "Watch history": "Riwayat tontonan", + "Delete account": "Hapus akun", + "Administrator preferences": "Preferensi administrator", + "Default homepage: ": "Laman beranda default: ", + "Feed menu: ": "Menu umpan: ", + "Top enabled: ": "", + "CAPTCHA enabled: ": "CAPTCHA diaktifkan: ", + "Login enabled: ": "Masuk diaktifkan: ", + "Registration enabled: ": "Registrasi diaktifkan: ", + "Report statistics: ": "Laporan statistik: ", + "Save preferences": "Simpan preferensi", + "Subscription manager": "Pengatur langganan", + "Token manager": "Pengatur token", + "Token": "Token", + "`x` subscriptions": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` langganan.([^.,0-9]|^)1([^.,0-9]|$)", + "": "`x` langganan." + }, + "`x` tokens": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` token.([^.,0-9]|^)1([^.,0-9]|$)", + "": "`x` token." + }, + "Import/export": "Impor/ekspor", + "unsubscribe": "batal langganan", + "revoke": "cabut", + "Subscriptions": "Langganan", + "`x` unseen notifications": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` pemberitahuan belum dilihat.([^.,0-9]|^)1([^.,0-9]|$)", + "": "`x` pemberitahuan belum dilihat." + }, + "search": "cari", + "Log out": "Keluar", + "Released under the AGPLv3 by Omar Roth.": "Dirilis dibawah AGPLv3 oleh Omar Roth.", + "Source available here.": "Sumber tersedia di sini.", + "View JavaScript license information.": "Tampilkan informasi lisensi JavaScript.", + "View privacy policy.": "Lihat kebijakan privasi.", + "Trending": "Sedang tren", + "Public": "Publik", + "Unlisted": "Tidak terdaftar", + "Private": "Pribadi", + "View all playlists": "Lihat semua daftar putar", + "Updated `x` ago": "Diperbarui`x` lalu", + "Delete playlist `x`?": "Hapus daftar putar `x`?", + "Delete playlist": "Hapus daftar putar", + "Create playlist": "Buat daftar putar", + "Title": "Judul", + "Playlist privacy": "Privasi daftar putar", + "Editing playlist `x`": "Menyunting daftar putar `x`", + "Watch on YouTube": "Tonton di YouTube", + "Hide annotations": "Sembunyikan anotasi", + "Show annotations": "Tampilkan anotasi", + "Genre: ": "Genre: ", + "License: ": "Lisensi: ", + "Family friendly? ": "Ramah keluarga? ", + "Wilson score: ": "Skor Wilson: ", + "Engagement: ": "Keterikatan: ", + "Whitelisted regions: ": "Wilayah daftar-putih: ", + "Blacklisted regions: ": "Wilayah daftar-hitam: ", + "Shared `x`": "Berbagi`x`", + "`x` views": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` tampilan.([^.,0-9]|^)1([^.,0-9]|$)", + "": "`x` tampilan." + }, + "Premieres in `x`": "Tayang dalam `x`", + "Premieres `x`": "Tayang `x`", + "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Hai! Kelihatannya JavaScript kamu dimatikan. Klik di sini untuk melihat komentar, perlu diingat hal ini mungkin membutuhkan waktu sedikit lebih lama untuk dimuat.", + "View YouTube comments": "Lihat komentar YouTube", + "View more comments on Reddit": "Lihat lebih banyak komentar di Reddit", + "View `x` comments": { + "([^.,0-9]|^)1([^.,0-9]|$)": "Lihat`x` komentar.([^.,0-9]|^)1([^.,0-9]|$)", + "": "Lihat`x` komentar." + }, + "View Reddit comments": "Lihat komentar Reddit", + "Hide replies": "Sembunyikan balasan", + "Show replies": "Lihat balasan", + "Incorrect password": "Kata sandi salah", + "Quota exceeded, try again in a few hours": "Kuota penuh, coba lagi dalam beberapa jam", + "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Tidak dapat masuk, pastikan autentikasi dua-faktor (autentikator atau SMS) sudah nyala.", + "Invalid TFA code": "Kode TFA tidak valid", + "Login failed. This may be because two-factor authentication is not turned on for your account.": "Gagal masuk. Ini mungkin disebabkan autentikasi dua-faktor tidak dinyalakan untuk akun Anda.", + "Wrong answer": "Jawaban salah", + "Erroneous CAPTCHA": "CAPTCHA salah", + "CAPTCHA is a required field": "CAPTCHA perlu diisi", + "User ID is a required field": "ID pengguna perlu diisi", + "Password is a required field": "Kata sandi perlu diisi", + "Wrong username or password": "Nama pengguna atau kata sandi salah", + "Please sign in using 'Log in with Google'": "Harap masuk menggunakan 'Masuk dengan Google'", + "Password cannot be empty": "Kata sandi tidak boleh kosong", + "Password cannot be longer than 55 characters": "Kata sandi tidak boleh lebih dari 55 karakter", + "Please log in": "Harap masuk", + "Invidious Private Feed for `x`": "Umpan pribadi Invidious untuk`x`", + "channel:`x`": "kanal:`x`", + "Deleted or invalid channel": "Kanal terhapus atau invalid", + "This channel does not exist.": "Kanal ini tidak ada.", + "Could not get channel info.": "Tidak bisa mendapatkan info kanal.", + "Could not fetch comments": "Tidak dapat memuat komentar", + "View `x` replies": { + "([^.,0-9]|^)1([^.,0-9]|$)": "Lihat`x` balasan.([^.,0-9]|^)1([^.,0-9]|$)", + "": "Lihat `x` balasan." + }, + "`x` ago": "`x` lalu", + "Load more": "Muat lebih banyak", + "`x` points": { + "([^.,0-9]|^)1([^.,0-9]|$)": "", + "": "" + }, + "Could not create mix.": "", + "Empty playlist": "Daftar putar kosong", + "Not a playlist.": "Bukan daftar putar.", + "Playlist does not exist.": "Daftar putar tidak ada.", + "Could not pull trending pages.": "Tidak bisa mendapatkan laman tren.", + "Hidden field \"challenge\" is a required field": "", + "Hidden field \"token\" is a required field": "", + "Erroneous challenge": "", + "Erroneous token": "", + "No such user": "Tidak ada pengguna demikian", + "Token is expired, please try again": "Token kadaluwarsa, harap coba lagi", + "English": "Bahasa Inggris", + "English (auto-generated)": "Bahasa Inggris (dibuat-otomatis)", + "Afrikaans": "Bahasa Afrika", + "Albanian": "Bahasa Albania", + "Amharic": "Bahasa Amharik", + "Arabic": "Bahasa arab", + "Armenian": "Bahasa Armenia", + "Azerbaijani": "", + "Bangla": "", + "Basque": "", + "Belarusian": "", + "Bosnian": "Bahasa Bosnia", + "Bulgarian": "Bahasa Bulgaria", + "Burmese": "Bahasa Birma", + "Catalan": "", + "Cebuano": "", + "Chinese (Simplified)": "", + "Chinese (Traditional)": "", + "Corsican": "", + "Croatian": "Bahasa Kroasia", + "Czech": "Bahasa Ceko", + "Danish": "", + "Dutch": "Bahasa Belanda", + "Esperanto": "", + "Estonian": "", + "Filipino": "", + "Finnish": "", + "French": "", + "Galician": "", + "Georgian": "", + "German": "", + "Greek": "Bahasa Yunani", + "Gujarati": "", + "Haitian Creole": "", + "Hausa": "", + "Hawaiian": "", + "Hebrew": "", + "Hindi": "", + "Hmong": "", + "Hungarian": "", + "Icelandic": "", + "Igbo": "", + "Indonesian": "Bahasa Indonesia", + "Irish": "", + "Italian": "", + "Japanese": "Bahasa Jepang", + "Javanese": "Bahasa Jawa", + "Kannada": "", + "Kazakh": "", + "Khmer": "", + "Korean": "Bahasa Korea", + "Kurdish": "", + "Kyrgyz": "", + "Lao": "", + "Latin": "", + "Latvian": "", + "Lithuanian": "", + "Luxembourgish": "", + "Macedonian": "", + "Malagasy": "", + "Malay": "Bahasa Melayu", + "Malayalam": "", + "Maltese": "", + "Maori": "", + "Marathi": "", + "Mongolian": "", + "Nepali": "", + "Norwegian Bokmål": "", + "Nyanja": "", + "Pashto": "", + "Persian": "", + "Polish": "", + "Portuguese": "", + "Punjabi": "", + "Romanian": "", + "Russian": "", + "Samoan": "", + "Scottish Gaelic": "", + "Serbian": "", + "Shona": "", + "Sindhi": "", + "Sinhala": "", + "Slovak": "", + "Slovenian": "", + "Somali": "", + "Southern Sotho": "", + "Spanish": "", + "Spanish (Latin America)": "", + "Sundanese": "Bahasa Sunda", + "Swahili": "", + "Swedish": "", + "Tajik": "", + "Tamil": "", + "Telugu": "", + "Thai": "Bahasa Thailand", + "Turkish": "", + "Ukrainian": "", + "Urdu": "", + "Uzbek": "", + "Vietnamese": "Bahasa Vietnam", + "Welsh": "", + "Western Frisian": "", + "Xhosa": "", + "Yiddish": "", + "Yoruba": "", + "Zulu": "", + "`x` years": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` tahun.([^.,0-9]|^)1([^.,0-9]|$)", + "": "`x` tahun." + }, + "`x` months": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` bulan.([^.,0-9]|^)1([^.,0-9]|$)", + "": "`x` bulan." + }, + "`x` weeks": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` pekan.([^.,0-9]|^)1([^.,0-9]|$)", + "": "`x` pekan." + }, + "`x` days": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` hari.([^.,0-9]|^)1([^.,0-9]|$)", + "": "`x` hari." + }, + "`x` hours": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` jam.([^.,0-9]|^)1([^.,0-9]|$)", + "": "`x` jam." + }, + "`x` minutes": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` menit.([^.,0-9]|^)1([^.,0-9]|$)", + "": "`x` menit." + }, + "`x` seconds": { + "([^.,0-9]|^)1([^.,0-9]|$)": "`x` detik.([^.,0-9]|^)1([^.,0-9]|$)", + "": "`x` detik." + }, + "Fallback comments: ": "", + "Popular": "Populer", + "Top": "", + "About": "Ihwal", + "Rating: ": "Peringkat: ", + "Language: ": "Bahasa: ", + "View as playlist": "Tampilkan sebagai daftar putar", + "Default": "Asali", + "Music": "Musik", + "Gaming": "Gaming", + "News": "Berita", + "Movies": "Film", + "Download": "Unduh", + "Download as: ": "Unduh sebagai: ", + "%A %B %-d, %Y": "", + "(edited)": "(disunting)", + "YouTube comment permalink": "", + "permalink": "", + "`x` marked it with a ❤": "`x` telah ditandai dengan ❤", + "Audio mode": "Mode audio", + "Video mode": "Mode video", + "Videos": "Video", + "Playlists": "Daftar putar", + "Community": "Komunitas", + "Current version: ": "Versi saat ini: " +} diff --git a/locales/is.json b/locales/is.json index 4cd15076..a2943b88 100644 --- a/locales/is.json +++ b/locales/is.json @@ -1,7 +1,7 @@ { - "`x` subscribers": "", - "`x` videos": "", - "`x` playlists": "", + "`x` subscribers": "`x`áskrifendur", + "`x` videos": "`x` myndbönd", + "`x` playlists": "`x` spilunarlistar", "`x` subscribers.": "`x` áskrifandar.", "`x` videos.": "`x` myndbönd.", "LIVE": "BEINT", @@ -71,11 +71,11 @@ "Show related videos: ": "Sýna tengd myndbönd? ", "Show annotations by default: ": "Á að sýna glósur sjálfgefið? ", "Visual preferences": "Sjónrænar stillingar", - "Player style: ": "", + "Player style: ": "Spilara stíl: ", "Dark mode: ": "Myrkur ham: ", - "Theme: ": "", - "dark": "", - "light": "", + "Theme: ": "Þema: ", + "dark": "dimmt", + "light": "ljóst", "Thin mode: ": "Þunnt ham: ", "Subscription preferences": "Áskriftarstillingar", "Show annotations by default for subscribed channels: ": "Á að sýna glósur sjálfgefið fyrir áskriftarrásir? ", @@ -113,13 +113,13 @@ "Report statistics: ": "Skrá talnagögn? ", "Save preferences": "Vista stillingar", "Subscription manager": "Áskriftarstjóri", - "`x` subscriptions": "", - "`x` tokens": "", + "`x` subscriptions": "`x` áskrifendur", + "`x` tokens": "`x` tákn", "Token manager": "Táknstjóri", "Token": "Tákn", "`x` subscriptions.": "`x` áskriftir.", "`x` tokens.": "`x` tákn.", - "`x` unseen notifications": "", + "`x` unseen notifications": "`x` óséðar tilkynningar", "Import/export": "Flytja inn/út", "unsubscribe": "afskrá", "revoke": "afturkalla", @@ -132,24 +132,24 @@ "View JavaScript license information.": "Skoða JavaScript leyfisupplýsingar.", "View privacy policy.": "Skoða meðferð persónuupplýsinga.", "Trending": "Vinsælt", - "Public": "", + "Public": "Opinbert", "Unlisted": "Óskráð", - "Private": "", - "View all playlists": "", - "Updated `x` ago": "", - "Delete playlist `x`?": "", - "Delete playlist": "", - "Create playlist": "", - "Title": "", - "Playlist privacy": "", - "Editing playlist `x`": "", + "Private": "Einka", + "View all playlists": "Skoða alla spilunarlista", + "Updated `x` ago": "Uppfært `x` síðann", + "Delete playlist `x`?": "Eiða spilunarlista `x`?", + "Delete playlist": "Eiða spilunarlista", + "Create playlist": "Búa til spilunarlista", + "Title": "Titill", + "Playlist privacy": "Spilunarlista opinberri", + "Editing playlist `x`": "Að breyta spilunarlista `x`", "Watch on YouTube": "Horfa á YouTube", "Hide annotations": "Fela glósur", "Show annotations": "Sýna glósur", "Genre: ": "Tegund: ", "License: ": "Notkunarleyfi: ", "Family friendly? ": "Fjölskylduvænt? ", - "`x` views": "", + "`x` views": "`x` áhorf", "Wilson score: ": "Wilson stig: ", "Engagement: ": "Þátttöku: ", "Whitelisted regions: ": "Svæði á hvítum lista: ", @@ -180,10 +180,10 @@ "Password cannot be empty": "Lykilorð má ekki vera autt", "Password cannot be longer than 55 characters": "Lykilorð má ekki vera lengra en 55 stafir", "Please log in": "Vinsamlegast skráðu þig inn", - "View `x` replies": "", + "View `x` replies": "Skoða `x` svör", "Invidious Private Feed for `x`": "Invidious Persónulegur Straumur fyrir `x`", "channel:`x`": "rás:`x`", - "`x` points": "", + "`x` points": "`x` stig", "Deleted or invalid channel": "Eytt eða ógild rás", "This channel does not exist.": "Þessi rás er ekki til.", "Could not get channel info.": "Ekki tókst að fá rásarupplýsingar.", @@ -301,13 +301,13 @@ "Turkish": "Tyrkneska", "Ukrainian": "Úkraníska", "Urdu": "Úrdú", - "`x` years": "", - "`x` months": "", - "`x` weeks": "", - "`x` days": "", - "`x` hours": "", - "`x` minutes": "", - "`x` seconds": "", + "`x` years": "`x` ár", + "`x` months": "`x` mánuði", + "`x` weeks": "`x` vikur", + "`x` days": "`x` daga", + "`x` hours": "`x` klukkustundir", + "`x` minutes": "`x` mínútur", + "`x` seconds": "`x` sekúndur", "Uzbek": "Úsbekíska", "Vietnamese": "Víetnamska", "Welsh": "Velska", @@ -325,13 +325,13 @@ "`x` seconds.": "`x` sekúndur.", "Fallback comments: ": "Vara ummæli: ", "Popular": "Vinsælt", - "permalink": "", + "permalink": "Varanlegur tengill", "Top": "Topp", "About": "Um", "Rating: ": "Einkunn: ", "Language: ": "Tungumál: ", "View as playlist": "Skoða sem spilunarlista", - "Community": "", + "Community": "Samfélag", "Default": "Sjálfgefið", "Music": "Tónlist", "Gaming": "Tólvuleikja", @@ -348,4 +348,4 @@ "Videos": "Myndbönd", "Playlists": "Spilunarlistar", "Current version: ": "Núverandi útgáfa: " -} \ No newline at end of file +} diff --git a/locales/it.json b/locales/it.json index 2e993c81..789bdd1a 100644 --- a/locales/it.json +++ b/locales/it.json @@ -1,12 +1,8 @@ { - "`x` subscribers.": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` iscritto", - "": "`x` iscritti." - }, - "`x` videos.": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` video", - "": "`x` video." - }, + "`x` subscribers..([^.,0-9]|^)1([^.,0-9]|$)": "`x` iscritto", + "`x` subscribers..": "`x` iscritti.", + "`x` videos..([^.,0-9]|^)1([^.,0-9]|$)": "`x` video", + "`x` videos..": "`x` video.", "`x` playlists": "`x` playlist", "LIVE": "IN DIRETTA", "Shared `x` ago": "Condiviso `x` fa", @@ -54,7 +50,7 @@ "Image CAPTCHA": "Immagine CAPTCHA", "Sign In": "Accedi", "Register": "Registrati", - "E-mail": "Email", + "E-mail": "E-mail", "Google verification code": "Codice di verifica Google", "Preferences": "Preferenze", "Player preferences": "Preferenze del riproduttore", @@ -119,22 +115,16 @@ "Subscription manager": "Gestione delle iscrizioni", "Token manager": "Gestione dei gettoni", "Token": "Gettone", - "`x` subscriptions.": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` iscrizione", - "": "`x` iscrizioni." - }, - "`x` tokens.": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` gettone", - "": "`x` gettoni." - }, + "`x` subscriptions..([^.,0-9]|^)1([^.,0-9]|$)": "`x` iscrizione", + "`x` subscriptions..": "`x` iscrizioni.", + "`x` tokens..([^.,0-9]|^)1([^.,0-9]|$)": "`x` gettone", + "`x` tokens..": "`x` gettoni.", "Import/export": "Importa/esporta", "unsubscribe": "disiscriviti", "revoke": "revoca", "Subscriptions": "Iscrizioni", - "`x` unseen notifications.": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` notifica non visualizzata", - "": "`x` notifiche non visualizzate." - }, + "`x` unseen notifications..([^.,0-9]|^)1([^.,0-9]|$)": "`x` notifica non visualizzata", + "`x` unseen notifications..": "`x` notifiche non visualizzate.", "search": "Cerca", "Log out": "Esci", "Released under the AGPLv3 by Omar Roth.": "Pubblicato con licenza AGPLv3 da Omar Roth.", @@ -164,10 +154,8 @@ "Whitelisted regions: ": "Regioni in lista bianca: ", "Blacklisted regions: ": "Regioni in lista nera: ", "Shared `x`": "Condiviso `x`", - "`x` views.": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` visualizzazione", - "": "`x` visualizzazioni." - }, + "`x` views..([^.,0-9]|^)1([^.,0-9]|$)": "`x` visualizzazione", + "`x` views..": "`x` visualizzazioni.", "Premieres in `x`": "In anteprima in `x`", "Premieres `x`": "In anteprima `x`", "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Ciao! Sembra che tu abbia disattivato JavaScript. Clicca qui per visualizzare i commenti. Considera che potrebbe volerci più tempo.", @@ -188,7 +176,7 @@ "User ID is a required field": "L'ID utente è obbligatorio", "Password is a required field": "La password è un campo obbligatorio", "Wrong username or password": "Nome utente o password errati", - "Please sign in using 'Log in with Google'": "Per favore accedi con \"Entra con Google\"", + "Please sign in using 'Log in with Google'": "Per favore accedi con «Entra con Google»", "Password cannot be empty": "La password non può essere vuota", "Password cannot be longer than 55 characters": "La password non può contenere più di 55 caratteri", "Please log in": "Per favore, accedi", @@ -198,24 +186,20 @@ "This channel does not exist.": "Questo canale non esiste.", "Could not get channel info.": "Impossibile ottenere le informazioni del canale.", "Could not fetch comments": "Impossibile recuperare i commenti", - "View `x` replies.": { - "([^.,0-9]|^)1([^.,0-9]|$)": "Visualizza `x` risposta", - "": "Visualizza `x` risposte." - }, + "View `x` replies..([^.,0-9]|^)1([^.,0-9]|$)": "Visualizza `x` risposta", + "View `x` replies..": "Visualizza `x` risposte.", "`x` ago": "`x` fa", "Load more": "Carica altro", - "`x` points.": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` punto", - "": "`x` punti." - }, + "`x` points..([^.,0-9]|^)1([^.,0-9]|$)": "`x` punto", + "`x` points..": "`x` punti.", "Could not create mix.": "Impossibile creare il mix.", "Empty playlist": "Playlist vuota", "Not a playlist.": "Non è una playlist.", "Playlist does not exist.": "La playlist non esiste.", "Could not pull trending pages.": "Impossibile recuperare le tendenze.", "Hidden field \"challenge\" is a required field": "Il campo nascosto \"challenge\" è obbligatorio", - "Hidden field \"token\" is a required field": "Il campo nascosto \"token\" è obbligatorio", - "Erroneous challenge": "Campo \"challenge\" non valido", + "Hidden field \"token\" is a required field": "Il campo nascosto «token» è obbligatorio", + "Erroneous challenge": "Campo «challenge» non valido", "Erroneous token": "Campo \"token\" non valido", "No such user": "Utente non valido", "Token is expired, please try again": "Gettone scaduto, riprova", @@ -288,7 +272,7 @@ "Nepali": "Nepalese", "Norwegian Bokmål": "Norvegese", "Nyanja": "Nyanja", - "Pashto": "Lingua pashtu", + "Pashto": "Pashtu", "Persian": "Persiano", "Polish": "Polacco", "Portuguese": "Portoghese", @@ -325,34 +309,20 @@ "Yiddish": "Yiddish", "Yoruba": "Yoruba", "Zulu": "Zulu", - "`x` years.": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` anno", - "": "`x` anni." - }, - "`x` months.": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` mese", - "": "`x` mesi." - }, - "`x` weeks.": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` settimana", - "": "`x` settimane." - }, - "`x` days.": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` giorno", - "": "`x` giorni." - }, - "`x` hours.": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` ora", - "": "`x` ore." - }, - "`x` minutes.": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` minuto", - "": "`x` minuti." - }, - "`x` seconds.": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` secondo", - "": "`x` secondi." - }, + "`x` years..([^.,0-9]|^)1([^.,0-9]|$)": "`x` anno", + "`x` years..": "`x` anni.", + "`x` months..([^.,0-9]|^)1([^.,0-9]|$)": "`x` mese", + "`x` months..": "`x` mesi.", + "`x` weeks..([^.,0-9]|^)1([^.,0-9]|$)": "`x` settimana", + "`x` weeks..": "`x` settimane.", + "`x` days..([^.,0-9]|^)1([^.,0-9]|$)": "`x` giorno", + "`x` days..": "`x` giorni.", + "`x` hours..([^.,0-9]|^)1([^.,0-9]|$)": "`x` ora", + "`x` hours..": "`x` ore.", + "`x` minutes..([^.,0-9]|^)1([^.,0-9]|$)": "`x` minuto", + "`x` minutes..": "`x` minuti.", + "`x` seconds..([^.,0-9]|^)1([^.,0-9]|$)": "`x` secondo", + "`x` seconds..": "`x` secondi.", "Fallback comments: ": "Commenti alternativi: ", "Popular": "Popolare", "Top": "Top", @@ -378,4 +348,4 @@ "Playlists": "Playlist", "Community": "Comunità", "Current version: ": "Versione attuale: " -} \ No newline at end of file +} diff --git a/locales/ja.json b/locales/ja.json index e9ca0e62..0c429d6b 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -201,7 +201,7 @@ "Invidious Private Feed for `x`": "`x` の Invidious プライベートフィード", "channel:`x`": "チャンネル:`x`", "Deleted or invalid channel": "削除済みまたは無効なチャンネルです", - "This channel does not exist.": "このチャンネルは存在していません", + "This channel does not exist.": "このチャンネルは存在しません。", "Could not get channel info.": "チャンネル情報を取得できませんでした。", "Could not fetch comments": "コメントを取得できませんでした", "View `x` replies": { @@ -217,7 +217,7 @@ "Could not create mix.": "ミックスを作成できませんでした。", "Empty playlist": "空の再生リスト", "Not a playlist.": "再生リストではありません。", - "Playlist does not exist.": "再生リストが存在していません・", + "Playlist does not exist.": "再生リストが存在しません。", "Could not pull trending pages.": "急上昇ページを取得できませんでした。", "Hidden field \"challenge\" is a required field": "非表示項目 \"challenge\" は必須項目です", "Hidden field \"token\" is a required field": "非表示項目 \"token\" は必須項目です", @@ -384,4 +384,4 @@ "Playlists": "プレイリスト", "Community": "コミュニティ", "Current version: ": "現在のバージョン: " -} \ No newline at end of file +} diff --git a/locales/nb-NO.json b/locales/nb-NO.json index ff40e27b..6bf5107b 100644 --- a/locales/nb-NO.json +++ b/locales/nb-NO.json @@ -132,12 +132,12 @@ "Private": "Privat", "View all playlists": "Vis alle spillelister", "Updated `x` ago": "Oppdatert `x` siden", - "Delete playlist `x`?": "Slett spillelisten `x`?", + "Delete playlist `x`?": "Slett spilleliste «x»?", "Delete playlist": "Slett spilleliste", "Create playlist": "Opprett spilleliste", "Title": "Tittel", "Playlist privacy": "Vern av spilleliste", - "Editing playlist `x`": "Redigerer spillelisten `x`", + "Editing playlist `x`": "Endre spilleliste «x»", "Watch on YouTube": "Vis video på YouTube", "Hide annotations": "Skjul merknader", "Show annotations": "Vis merknader", @@ -174,7 +174,7 @@ "Password cannot be empty": "Passordet kan ikke være tomt", "Password cannot be longer than 55 characters": "Passordet kan ikke være lengre enn 55 tegn", "Please log in": "Logg inn", - "Invidious Private Feed for `x`": "Ugyldig privat flyt for `x`", + "Invidious Private Feed for `x`": "Invidious personlige flyt for `x`", "channel:`x`": "kanal `x`", "Deleted or invalid channel": "Slettet eller ugyldig kanal", "This channel does not exist.": "Denne kanalen finnes ikke.", @@ -203,18 +203,18 @@ "Arabic": "Arabisk", "Armenian": "Armensk", "Azerbaijani": "Aserbajdsjansk", - "Bangla": "", - "Basque": "", + "Bangla": "Bengali", + "Basque": "Baskisk", "Belarusian": "Hviterussisk", "Bosnian": "Bosnisk", "Bulgarian": "Bulgarsk", "Burmese": "Burmesisk", "Catalan": "Katalansk", - "Cebuano": "", - "Chinese (Simplified)": "", - "Chinese (Traditional)": "", - "Corsican": "", - "Croatian": "", + "Cebuano": "Sugboanon", + "Chinese (Simplified)": "Forenklet kinesisk", + "Chinese (Traditional)": "Tradisjonell kinesisk", + "Corsican": "Korsikansk", + "Croatian": "Kroatisk", "Czech": "Tsjekkisk", "Danish": "Dansk", "Dutch": "Nederlandsk", @@ -223,84 +223,84 @@ "Filipino": "Filippinsk", "Finnish": "Finsk", "French": "Fransk", - "Galician": "", - "Georgian": "", + "Galician": "Galisisk", + "Georgian": "Georgisk", "German": "Tysk", "Greek": "Gresk", - "Gujarati": "", - "Haitian Creole": "", - "Hausa": "", - "Hawaiian": "", - "Hebrew": "", - "Hindi": "", - "Hmong": "", + "Gujarati": "Gujarati", + "Haitian Creole": "Haitisk kreol", + "Hausa": "Hausa", + "Hawaiian": "Hawaiisk", + "Hebrew": "Hebraisk", + "Hindi": "Hindi", + "Hmong": "Hmong", "Hungarian": "Ungarsk", "Icelandic": "Islandsk", - "Igbo": "", + "Igbo": "Ibo", "Indonesian": "Indonesisk", "Irish": "Irsk", "Italian": "Italiensk", "Japanese": "Japansk", - "Javanese": "", - "Kannada": "", - "Kazakh": "", - "Khmer": "", - "Korean": "", - "Kurdish": "", - "Kyrgyz": "", - "Lao": "", - "Latin": "", - "Latvian": "", - "Lithuanian": "", - "Luxembourgish": "", - "Macedonian": "", - "Malagasy": "", - "Malay": "", - "Malayalam": "", - "Maltese": "", - "Maori": "", - "Marathi": "", - "Mongolian": "", - "Nepali": "", + "Javanese": "Javanesisk", + "Kannada": "Kanaresisk", + "Kazakh": "Kasakhisk", + "Khmer": "Khmer", + "Korean": "Koreansk", + "Kurdish": "Kurdisk", + "Kyrgyz": "Kirgisisk", + "Lao": "Laotisk", + "Latin": "Latin", + "Latvian": "Latvisk", + "Lithuanian": "Litauisk", + "Luxembourgish": "Luxemburgsk", + "Macedonian": "Makedonsk", + "Malagasy": "Madagassisk", + "Malay": "Malayisk", + "Malayalam": "Malayalam", + "Maltese": "Maltesisk", + "Maori": "Maorisk", + "Marathi": "Marathi", + "Mongolian": "Mongolsk", + "Nepali": "Gurkhali", "Norwegian Bokmål": "Norsk bokmål", - "Nyanja": "", - "Pashto": "", - "Persian": "", - "Polish": "", - "Portuguese": "", - "Punjabi": "", - "Romanian": "", + "Nyanja": "Nyanja", + "Pashto": "Pukhto", + "Persian": "Persisk", + "Polish": "Polsk", + "Portuguese": "Portugisisk", + "Punjabi": "Panjabi", + "Romanian": "Rumensk", "Russian": "Russisk", - "Samoan": "", - "Scottish Gaelic": "", + "Samoan": "Samoansk", + "Scottish Gaelic": "Skotsk-gælisk", "Serbian": "Serbisk", - "Shona": "", - "Sindhi": "", - "Sinhala": "", + "Shona": "Shona", + "Sindhi": "Sindhī", + "Sinhala": "Singalesisk", "Slovak": "Slovakisk", "Slovenian": "Slovensk", "Somali": "Somali", - "Southern Sotho": "", + "Southern Sotho": "Sørsotho", "Spanish": "Spansk", - "Spanish (Latin America)": "", - "Sundanese": "", - "Swahili": "", + "Spanish (Latin America)": "Spansk (Latin-Amerika)", + "Sundanese": "Sundanesisk", + "Swahili": "Kiswahili", "Swedish": "Svensk", - "Tajik": "", - "Tamil": "", - "Telugu": "", - "Thai": "", + "Tajik": "Tadsjikisk", + "Tamil": "Tamil", + "Telugu": "Telugu", + "Thai": "Thai", "Turkish": "Tyrkisk", "Ukrainian": "Ukrainsk", - "Urdu": "", - "Uzbek": "", + "Urdu": "Lashkari", + "Uzbek": "Usbekisk", "Vietnamese": "Vietnamesisk", - "Welsh": "", - "Western Frisian": "", - "Xhosa": "", - "Yiddish": "", - "Yoruba": "", - "Zulu": "", + "Welsh": "Velsk", + "Western Frisian": "Vestfrisisk", + "Xhosa": "Xhosa", + "Yiddish": "Jiddisk", + "Yoruba": "Joruba", + "Zulu": "Zulu", "`x` years": "`x` år", "`x` months": "`x` måneder", "`x` weeks": "`x` uker", @@ -322,7 +322,7 @@ "Movies": "Filmer", "Download": "Last ned", "Download as: ": "Last ned som: ", - "%A %B %-d, %Y": "", + "%A %B %-d, %Y": "%A %B %-d, %Y", "(edited)": "(redigert)", "YouTube comment permalink": "Permanent YouTube-lenke til innholdet", "permalink": "permanent lenke", @@ -332,5 +332,5 @@ "Videos": "Videoer", "Playlists": "Spillelister", "Community": "Gemenskap", - "Current version: ": "Nåværende versjon: " -} \ No newline at end of file + "Current version: ": "Gjeldende versjon: " +} diff --git a/locales/nl.json b/locales/nl.json index 29af954a..de2a2bb7 100644 --- a/locales/nl.json +++ b/locales/nl.json @@ -69,7 +69,7 @@ "Show related videos: ": "Gerelateerde video's tonen? ", "Show annotations by default: ": "Standaard annotaties tonen? ", "Visual preferences": "Visuele instellingen", - "Player style: ": "Speler vormgeving", + "Player style: ": "Speler vormgeving ", "Dark mode: ": "Donkere modus: ", "Theme: ": "Thema: ", "dark": "donker", @@ -103,7 +103,7 @@ "Delete account": "Account verwijderen", "Administrator preferences": "Beheerdersinstellingen", "Default homepage: ": "Standaard startpagina: ", - "Feed menu: ": "Feedmenu:", + "Feed menu: ": "Feedmenu: ", "Top enabled: ": "Bovenkant inschakelen? ", "CAPTCHA enabled: ": "CAPTCHA gebruiken? ", "Login enabled: ": "Inloggen toestaan? ", @@ -125,7 +125,7 @@ "Released under the AGPLv3 by Omar Roth.": "Uitgebracht onder de AGPLv3-licentie, door Omar Roth.", "Source available here.": "De broncode is hier beschikbaar.", "View JavaScript license information.": "JavaScript-licentieinformatie tonen.", - "View privacy policy.": "Privacybeleid tonen", + "View privacy policy.": "Privacybeleid tonen.", "Trending": "Uitgelicht", "Public": "Publiek", "Unlisted": "Verborgen", @@ -151,7 +151,7 @@ "Shared `x`": "`x` gedeeld", "`x` views": "`x` weergaven", "Premieres in `x`": "Verschijnt over `x`", - "Premieres `x`": "", + "Premieres `x`": "Verschijnt op `x`", "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Hoi! Het lijkt erop dat je JavaScript hebt uitgeschakeld. Klik hier om de reacties te bekijken. Let op: het laden duurt wat langer.", "View YouTube comments": "YouTube-reacties tonen", "View more comments on Reddit": "Meer reacties bekijken op Reddit", @@ -308,16 +308,16 @@ "`x` hours": "`x` uur", "`x` minutes": "`x` minuten", "`x` seconds": "`x` seconden", - "Fallback comments: ": "Terugvallen op", + "Fallback comments: ": "Terugvallen op ", "Popular": "Populair", "Top": "Top", "About": "Over", - "Rating: ": "Waardering", - "Language: ": "Taal", + "Rating: ": "Waardering: ", + "Language: ": "Taal: ", "View as playlist": "Tonen als afspeellijst", "Default": "Standaard", "Music": "Muziek", - "Gaming": "Gaming", + "Gaming": "Gamen", "News": "Nieuws", "Movies": "Films", "Download": "Downloaden", @@ -325,7 +325,7 @@ "%A %B %-d, %Y": "%A %B %-d, %Y", "(edited)": "(bewerkt)", "YouTube comment permalink": "Link naar YouTube-reactie", - "permalink": "", + "permalink": "permalink", "`x` marked it with a ❤": "`x` heeft dit gemarkeerd met ❤", "Audio mode": "Audiomodus", "Video mode": "Videomodus", @@ -334,4 +334,4 @@ "Community": "Gemeenschap", "Current version: ": "Huidige versie: ", "Download is disabled.": "Downloaden is uitgeschakeld." -} \ No newline at end of file +} diff --git a/locales/pl.json b/locales/pl.json index 32ff0530..66b8f4b0 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -48,7 +48,7 @@ "Image CAPTCHA": "Obraz CAPTCHA", "Sign In": "Zaloguj się", "Register": "Zarejestruj się", - "E-mail": "Email", + "E-mail": "E-mail", "Google verification code": "Kod weryfikacyjny Google", "Preferences": "Preferencje", "Player preferences": "Ustawienia odtwarzacza", @@ -322,7 +322,7 @@ "Movies": "Filmy", "Download": "Pobierz", "Download as: ": "Pobierz jako: ", - "%A %B %-d, %Y": "", + "%A %B %-d, %Y": "%A, %-d %B %Y", "(edited)": "(edytowany)", "YouTube comment permalink": "Odnośnik bezpośredni do komentarza na YouTube", "permalink": "bezpośredni odnośnik", @@ -333,4 +333,4 @@ "Playlists": "Playlisty", "Community": "Społeczność", "Current version: ": "Aktualna wersja: " -} \ No newline at end of file +} diff --git a/locales/pt-BR.json b/locales/pt-BR.json index 9dd237c6..cf73abd8 100644 --- a/locales/pt-BR.json +++ b/locales/pt-BR.json @@ -1,10 +1,10 @@ { "`x` subscribers": "`x` inscritos", - "`x` videos": "`x` videos", - "`x` playlists": "`x` lista de reprodução", + "`x` videos": "`x` vídeos", + "`x` playlists": "`x` listas de reprodução", "LIVE": "AO VIVO", "Shared `x` ago": "Compartilhado `x` atrás", - "Unsubscribe": "Desinscrever-se", + "Unsubscribe": "Cancelar inscrição", "Subscribe": "Inscrever-se", "View channel on YouTube": "Ver canal no YouTube", "View playlist on YouTube": "Ver lista de reprodução no YouTube", @@ -17,14 +17,14 @@ "Clear watch history?": "Limpar histórico de reprodução?", "New password": "Nova senha", "New passwords must match": "Nova senha deve ser igual", - "Cannot change password for Google accounts": "Não é possível alterar sua senha da conta Google", + "Cannot change password for Google accounts": "Não é possível alterar sua senha de contas do Google", "Authorize token?": "Autorizar o token?", "Authorize token for `x`?": "Autorizar o token para `x`?", "Yes": "Sim", "No": "Não", "Import and Export Data": "Importar e Exportar Dados", "Import": "Importar", - "Import Invidious data": "Importar datos do Invidious", + "Import Invidious data": "Importar dados do Invidious", "Import YouTube subscriptions": "Importar inscrições do YouTube", "Import FreeTube subscriptions (.db)": "Importar inscrições do FreeTube (.db)", "Import NewPipe subscriptions (.json)": "Importar inscrições do NewPipe (.json)", @@ -33,11 +33,11 @@ "Export subscriptions as OPML": "Exportar inscrições como OPML", "Export subscriptions as OPML (for NewPipe & FreeTube)": "Exportar inscrições como OPML (para NewPipe e FreeTube)", "Export data as JSON": "Exportar dados como JSON", - "Delete account?": "Deletar conta?", + "Delete account?": "Excluir conta?", "History": "Histórico", "An alternative front-end to YouTube": "Uma interface alternativa para o YouTube", "JavaScript license information": "Informação de licença do JavaScript", - "source": "código fonte", + "source": "código-fonte", "Log in": "Entrar", "Log in/register": "Entrar/Registrar", "Log in with Google": "Entrar com conta Google", @@ -45,7 +45,7 @@ "Password": "Senha", "Time (h:mm:ss):": "Hora (h:mm:ss):", "Text CAPTCHA": "CAPTCHA em texto", - "Image CAPTCHA": "CAPTCHA em imagen", + "Image CAPTCHA": "CAPTCHA em imagem", "Sign In": "Entrar", "Register": "Registrar", "E-mail": "E-mail", @@ -55,43 +55,43 @@ "Always loop: ": "Repetir sempre: ", "Autoplay: ": "Reprodução automática: ", "Play next by default: ": "Sempre reproduzir próximo: ", - "Autoplay next video: ": "Reproduzir próximo video automaticamente: ", - "Listen by default: ": "Sempre ativar som: ", - "Proxy videos: ": "Usar proxy nos videos: ", - "Default speed: ": "Velocidade preferida: ", - "Preferred video quality: ": "Qualidade de video preferida: ", + "Autoplay next video: ": "Reproduzir próximo vídeo automaticamente: ", + "Listen by default: ": "Apenas áudio por padrão: ", + "Proxy videos: ": "Usar proxy nos vídeos: ", + "Default speed: ": "Velocidade padrão: ", + "Preferred video quality: ": "Qualidade de vídeo preferida: ", "Player volume: ": "Volume de reprodução: ", "Default comments: ": "Preferência de comentários: ", "youtube": "youtube", "reddit": "reddit", "Default captions: ": "Preferência de legendas: ", "Fallback captions: ": "Legendas alternativas: ", - "Show related videos: ": "Ver videos relacionados: ", + "Show related videos: ": "Mostrar vídeos relacionados: ", "Show annotations by default: ": "Sempre mostrar anotações: ", "Visual preferences": "Preferências visuais", - "Player style: ": "Estilo do reprodutor", + "Player style: ": "Estilo do tocador: ", "Dark mode: ": "Modo escuro: ", - "Theme: ": "Tema", + "Theme: ": "Tema: ", "dark": "escuro", "light": "claro", "Thin mode: ": "Modo compacto: ", "Subscription preferences": "Preferências de inscrições", - "Show annotations by default for subscribed channels: ": "Sempre mostrar anotações nos videos de canais inscritos ", + "Show annotations by default for subscribed channels: ": "Sempre mostrar anotações dos vídeos de canais inscritos: ", "Redirect homepage to feed: ": "Redirecionar página inicial para o feed: ", - "Number of videos shown in feed: ": "Número de videos no feed: ", - "Sort videos by: ": "Ordenar videos por: ", + "Number of videos shown in feed: ": "Número de vídeos no feed: ", + "Sort videos by: ": "Ordenar vídeos por: ", "published": "publicado", "published - reverse": "publicado - ordem inversa", "alphabetically": "alfabética", "alphabetically - reverse": "alfabética - ordem inversa", "channel name": "nome do canal", "channel name - reverse": "nome do canal - ordem inversa", - "Only show latest video from channel: ": "Mostrar apenas o video mais recente do canal: ", - "Only show latest unwatched video from channel: ": "Mostrar apenas o video mais recente não visualizados do canal: ", - "Only show unwatched: ": "Mostrar apenas videos não visualizados: ", + "Only show latest video from channel: ": "Mostrar apenas o vídeo mais recente do canal: ", + "Only show latest unwatched video from channel: ": "Mostrar apenas o vídeo mais recente não visualizado do canal: ", + "Only show unwatched: ": "Mostrar apenas vídeos não visualizados: ", "Only show notifications (if there are any): ": "Mostrar apenas notificações (se existentes): ", "Enable web notifications": "Ativar notificações pela web", - "`x` uploaded a video": "`x` publicou um novo video", + "`x` uploaded a video": "`x` publicou um novo vídeo", "`x` is live": "`x` está ao vivo", "Data preferences": "Preferências de dados", "Clear watch history": "Limpar histórico de reprodução", @@ -102,8 +102,8 @@ "Watch history": "Histórico de reprodução", "Delete account": "Apagar sua conta", "Administrator preferences": "Preferências de administrador", - "Default homepage: ": "Página de inicio padrão: ", - "Feed menu: ": "Menú do feed: ", + "Default homepage: ": "Página de início padrão: ", + "Feed menu: ": "Menu do feed: ", "Top enabled: ": "Habilitar destaques: ", "CAPTCHA enabled: ": "Habilitar CAPTCHA: ", "Login enabled: ": "Habilitar login: ", @@ -116,64 +116,64 @@ "`x` subscriptions": "`x` inscrições", "`x` tokens": "`x` tokens", "Import/export": "Importar/Exportar", - "unsubscribe": "desinscrever-se", + "unsubscribe": "cancelar inscrição", "revoke": "revogar", "Subscriptions": "Inscrições", "`x` unseen notifications": "`x` notificações não visualizadas", - "search": "procurar", + "search": "Pesquisar", "Log out": "Sair", "Released under the AGPLv3 by Omar Roth.": "Publicado sob a licença AGPLv3, por Omar Roth.", - "Source available here.": "Código fonte disponível aqui.", + "Source available here.": "Código-fonte disponível aqui.", "View JavaScript license information.": "Ver informações da licença do JavaScript.", - "View privacy policy.": "Ver a política de privacidade", - "Trending": "Trending", + "View privacy policy.": "Ver a política de privacidade.", + "Trending": "Tendências", "Public": "Público", - "Unlisted": "No listado", + "Unlisted": "Não listado", "Private": "Privado", "View all playlists": "Mostrar todas listas de reprodução", - "Updated `x` ago": "Enviado `x` atrás", + "Updated `x` ago": "Atualizado `x` atrás", "Delete playlist `x`?": "Apagar a playlist `x`?", "Delete playlist": "Apagar playlist", "Create playlist": "Criar playlist", "Title": "Título", "Playlist privacy": "Privacidade da playlist", - "Editing playlist `x`": "Editando playlist", - "Watch on YouTube": "Assistir vídeo no YouTube", + "Editing playlist `x`": "Editando playlist `x`", + "Watch on YouTube": "Assistir no YouTube", "Hide annotations": "Ocultar anotações", "Show annotations": "Mostrar anotações", "Genre: ": "Gênero: ", "License: ": "Licença: ", - "Family friendly? ": "Fistrar conteúdo impróprio: ", + "Family friendly? ": "Filtrar conteúdo impróprio: ", "Wilson score: ": "Pontuação de Wilson: ", - "Engagement: ": "Engagement: ", + "Engagement: ": "Empenho: ", "Whitelisted regions: ": "Regiões permitidas: ", "Blacklisted regions: ": "Regiões bloqueadas: ", "Shared `x`": "Compartilhado `x`", "`x` views": "`x` visualizações", - "Premieres in `x`": "Estreias em `x`", + "Premieres in `x`": "Estreia em `x`", "Premieres `x`": "Estreia `x`", "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Oi! Parece que seu JavaScript está desativado. Clique aqui para ver os comentários, entretanto eles podem levar um pouco mais de tempo para carregar.", - "View YouTube comments": "Ver comentários do YouTube", - "View more comments on Reddit": "Ver mais comentários do Reddit", + "View YouTube comments": "Ver comentários no YouTube", + "View more comments on Reddit": "Ver mais comentários no Reddit", "View `x` comments": "Ver `x` comentários", - "View Reddit comments": "Ver comentários do Reddit", + "View Reddit comments": "Ver comentários no Reddit", "Hide replies": "Ocultar respostas", "Show replies": "Mostrar respostas", "Incorrect password": "Senha incorreta", "Quota exceeded, try again in a few hours": "Cota excedida, tente novamente em algumas horas", - "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Não foi possível fazer login, sua autenticação por dois passos (app autenticador ou sms) deve estar ativada.", + "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Não foi possível fazer login, sua autenticação em dois passos (app autenticador ou sms) deve estar ativada.", "Invalid TFA code": "Código TFA inválido", - "Login failed. This may be because two-factor authentication is not turned on for your account.": "Falha no login. Isso pode acontecer pois a autenticação por dois passos está desativada para sua conta.", - "Wrong answer": "Respuesta inválida", + "Login failed. This may be because two-factor authentication is not turned on for your account.": "Falha no login. Isso pode acontecer porque a autenticação em dois passos está desativada para sua conta.", + "Wrong answer": "Resposta incorreta", "Erroneous CAPTCHA": "CAPTCHA inválido", "CAPTCHA is a required field": "O CAPTCHA é um campo obrigatório", "User ID is a required field": "O nome de usuário é um campo obrigatório", "Password is a required field": "A senha é um campo obrigatório", "Wrong username or password": "Nome de usuário ou senha inválidos", "Please sign in using 'Log in with Google'": "Por favor, entre usando 'Entrar com conta Google'", - "Password cannot be empty": "A senha não pode estar vazia", + "Password cannot be empty": "A senha não pode ficar em branco", "Password cannot be longer than 55 characters": "A senha não pode ter mais que 55 caracteres", - "Please log in": "Por favor, inicie sua seção", + "Please log in": "Por favor, inicie sua sessão", "Invidious Private Feed for `x`": "Feed Privado do Invidious para `x`", "channel:`x`": "canal: `x`", "Deleted or invalid channel": "Este canal foi apagado ou é inválido", @@ -185,15 +185,15 @@ "Load more": "Carregar mais", "`x` points": "`x` pontos", "Could not create mix.": "Não foi possível criar o mix.", - "Empty playlist": "A lista de reprodução está vazia", - "Not a playlist.": "Lista de reprodução inválida.", + "Empty playlist": "Lista de reprodução vazia", + "Not a playlist.": "Não é uma lista de reprodução.", "Playlist does not exist.": "A lista de reprodução não existe.", - "Could not pull trending pages.": "Não foi possível oberter as páginas dos videos em alta.", + "Could not pull trending pages.": "Não foi possível obter as páginas dos vídeos em alta.", "Hidden field \"challenge\" is a required field": "O campo oculto \"desafio\" é obrigatório", "Hidden field \"token\" is a required field": "O campo oculto \"token\" é obrigatório", - "Erroneous challenge": "Desafío inválido", - "Erroneous token": "Símbolo inválido", - "No such user": "Usuario inválido", + "Erroneous challenge": "Desafio inválido", + "Erroneous token": "Token inválido", + "No such user": "Usuário inválido", "Token is expired, please try again": "Token expirou, tente novamente", "English": "Inglês", "English (auto-generated)": "Inglês (gerado automaticamente)", @@ -206,7 +206,7 @@ "Bangla": "Bengalês", "Basque": "Basco", "Belarusian": "Bielorrusso", - "Bosnian": "Língua Bósnia", + "Bosnian": "Bósnio", "Bulgarian": "Búlgaro", "Burmese": "Birmanês", "Catalan": "Catalão", @@ -242,7 +242,7 @@ "Italian": "Italiano", "Japanese": "Japonês", "Javanese": "Javanês", - "Kannada": "Canarẽs", + "Kannada": "Canarês", "Kazakh": "Cazaque", "Khmer": "Khmer", "Korean": "Coreano", @@ -266,26 +266,26 @@ "Nyanja": "Nianja", "Pashto": "Pachto", "Persian": "Persa", - "Polish": "Polaco", + "Polish": "Polonês", "Portuguese": "Português", "Punjabi": "Panjábi", - "Romanian": "Língua Romena", + "Romanian": "Romeno", "Russian": "Russo", "Samoan": "Samoano", "Scottish Gaelic": "Ânglico Escocês", - "Serbian": "Língua Sérvia", + "Serbian": "Sérvio", "Shona": "Xona", "Sindhi": "Sindi", "Sinhala": "Cingalês", "Slovak": "Eslovaco", "Slovenian": "Esloveno", - "Somali": "Língua Somalí", + "Somali": "Somali", "Southern Sotho": "Sesoto", "Spanish": "Espanhol", - "Spanish (Latin America)": "Espanhol (América)", - "Sundanese": "Sondanese", + "Spanish (Latin America)": "Espanhol (América Latina)", + "Sundanese": "Sundanês", "Swahili": "Suaíli", - "Swedish": "Suéco", + "Swedish": "Sueco", "Tajik": "Tajiques", "Tamil": "Tâmil", "Telugu": "Telugo", @@ -300,7 +300,7 @@ "Xhosa": "Xhosa", "Yiddish": "Iídiche", "Yoruba": "Iorubá", - "Zulu": "Língua Zulú", + "Zulu": "Zulu", "`x` years": "`x` anos", "`x` months": "`x` meses", "`x` weeks": "`x` semanas", @@ -315,22 +315,22 @@ "Rating: ": "Avaliação: ", "Language: ": "Idioma: ", "View as playlist": "Ver como lista de reprodução", - "Default": "Configuração padrão", - "Music": "Música", - "Gaming": "Video Games", + "Default": "Padrão", + "Music": "Músicas", + "Gaming": "Jogos", "News": "Notícias", "Movies": "Filmes", "Download": "Baixar", "Download as: ": "Baixar como: ", "%A %B %-d, %Y": "%A %-d %B %Y", "(edited)": "(editado)", - "YouTube comment permalink": "Link permanente do comentário do YouTube", + "YouTube comment permalink": "Link permanente do comentário no YouTube", "permalink": "Link permanente", "`x` marked it with a ❤": "`x` foi marcado como ❤", - "Audio mode": "Modo de audio", - "Video mode": "Modo de video", + "Audio mode": "Modo de áudio", + "Video mode": "Modo de vídeo", "Videos": "Vídeos", "Playlists": "Listas de reprodução", "Community": "Comunidade", "Current version: ": "Versão atual: " -} \ No newline at end of file +} diff --git a/locales/pt-PT.json b/locales/pt-PT.json index ab7d3468..1082c023 100644 --- a/locales/pt-PT.json +++ b/locales/pt-PT.json @@ -1,16 +1,10 @@ { - "`x` subscribers.": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` subscritores.", - "": "`x` subscritores." - }, - "`x` videos.": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` vídeos.", - "": "`x` vídeos." - }, - "`x` playlists.": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` listas de reprodução.", - "": "`x` listas de reprodução." - }, + "`x` subscribers..([^.,0-9]|^)1([^.,0-9]|$)": "`x` subscritores..([^.,0-9]|^)1([^.,0-9]|$)", + "`x` subscribers..": "`x` subscritores.", + "`x` videos..([^.,0-9]|^)1([^.,0-9]|$)": "`x` videos..([^.,0-9]|^)1([^.,0-9]|$)", + "`x` videos..": "`x` vídeos.", + "`x` playlists..([^.,0-9]|^)1([^.,0-9]|$)": "`x` listas de reprodução.", + "`x` playlists..": "`x` listas de reprodução.", "LIVE": "Em direto", "Shared `x` ago": "Partilhado `x` atrás", "Unsubscribe": "Anular subscrição", @@ -26,7 +20,7 @@ "Clear watch history?": "Limpar histórico de reprodução?", "New password": "Nova palavra-chave", "New passwords must match": "As novas palavra-chaves devem corresponder", - "Cannot change password for Google accounts": "Não é possível alterar palavra-chave para contas do Google", + "Cannot change password for Google accounts": "Não é possível alterar a palavra-passe para contas do Google", "Authorize token?": "Autorizar token?", "Authorize token for `x`?": "Autorizar token para `x`?", "Yes": "Sim", @@ -42,9 +36,9 @@ "Export subscriptions as OPML": "Exportar subscrições como OPML", "Export subscriptions as OPML (for NewPipe & FreeTube)": "Exportar subscrições como OPML (para NewPipe e FreeTube)", "Export data as JSON": "Exportar dados como JSON", - "Delete account?": "Eliminar conta?", + "Delete account?": "Apagar conta?", "History": "Histórico", - "An alternative front-end to YouTube": "Uma interface alternativa para o YouTube", + "An alternative front-end to YouTube": "Uma interface alternativa ao YouTube", "JavaScript license information": "Informação de licença do JavaScript", "source": "código-fonte", "Log in": "Iniciar sessão", @@ -85,9 +79,9 @@ "light": "claro", "Thin mode: ": "Modo compacto: ", "Subscription preferences": "Preferências de subscrições", - "Show annotations by default for subscribed channels: ": "Mostrar sempre anotações para os canais subscritos: ", + "Show annotations by default for subscribed channels: ": "Mostrar sempre anotações aos canais subscritos: ", "Redirect homepage to feed: ": "Redirecionar página inicial para subscrições: ", - "Number of videos shown in feed: ": "Número de vídeos nas subscrições: ", + "Number of videos shown in feed: ": "Quantidade de vídeos nas subscrições: ", "Sort videos by: ": "Ordenar vídeos por: ", "published": "publicado", "published - reverse": "publicado - inverso", @@ -109,9 +103,9 @@ "Manage subscriptions": "Gerir as subscrições", "Manage tokens": "Gerir tokens", "Watch history": "Histórico de reprodução", - "Delete account": "Eliminar conta", + "Delete account": "Apagar conta", "Administrator preferences": "Preferências de administrador", - "Default homepage: ": "Página inicial padrão: ", + "Default homepage: ": "Página inicial predefinida: ", "Feed menu: ": "Menu de subscrições: ", "Top enabled: ": "Top ativado: ", "CAPTCHA enabled: ": "CAPTCHA ativado: ", @@ -122,22 +116,16 @@ "Subscription manager": "Gerir subscrições", "Token manager": "Gerir tokens", "Token": "Token", - "`x` subscriptions.": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` subscrições.", - "": "`x` subscrições." - }, - "`x` tokens.": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` tokens.", - "": "`x` tokens." - }, + "`x` subscriptions..([^.,0-9]|^)1([^.,0-9]|$)": "`x` subscrições.", + "`x` subscriptions..": "`x` subscrições.", + "`x` tokens..([^.,0-9]|^)1([^.,0-9]|$)": "`x` tokens.", + "`x` tokens..": "`x` tokens.", "Import/export": "Importar/Exportar", "unsubscribe": "Anular subscrição", "revoke": "revogar", "Subscriptions": "Subscrições", - "`x` unseen notifications.": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` notificações não vistas.", - "": "`x` notificações não vistas." - }, + "`x` unseen notifications..([^.,0-9]|^)1([^.,0-9]|$)": "`x` notificações não vistas.", + "`x` unseen notifications..": "`x` notificações não vistas.", "search": "Pesquisar", "Log out": "Terminar sessão", "Released under the AGPLv3 by Omar Roth.": "Publicado sob a licença AGPLv3, por Omar Roth.", @@ -150,8 +138,8 @@ "Private": "Privado", "View all playlists": "Ver todas as listas de reprodução", "Updated `x` ago": "Atualizado `x` atrás", - "Delete playlist `x`?": "Eliminar a lista de reprodução 'x'?", - "Delete playlist": "Eliminar lista de reprodução", + "Delete playlist `x`?": "Apagar a lista de reprodução 'x'?", + "Delete playlist": "Apagar lista de reprodução", "Create playlist": "Criar lista de reprodução", "Title": "Título", "Playlist privacy": "Privacidade da lista de reprodução", @@ -167,19 +155,15 @@ "Whitelisted regions: ": "Regiões permitidas: ", "Blacklisted regions: ": "Regiões bloqueadas: ", "Shared `x`": "Partilhado `x`", - "`x` views.": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` visualizações.", - "": "`x` visualizações." - }, + "`x` views..([^.,0-9]|^)1([^.,0-9]|$)": "`x` visualizações.", + "`x` views..": "`x` visualizações.", "Premieres in `x`": "Estreias em 'x'", "Premieres `x`": "Estreias 'x'", "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Oi! Parece que JavaScript está desativado. Clique aqui para ver os comentários, entretanto eles podem levar mais tempo para carregar.", "View YouTube comments": "Ver comentários do YouTube", "View more comments on Reddit": "Ver mais comentários no Reddit", - "View `x` comments.": { - "([^.,0-9]|^)1([^.,0-9]|$)": "Ver `x` comentários.", - "": "Ver `x` comentários." - }, + "View `x` comments..([^.,0-9]|^)1([^.,0-9]|$)": "Ver `x` comentários.", + "View `x` comments..": "Ver `x` comentários.", "View Reddit comments": "Ver comentários do Reddit", "Hide replies": "Ocultar respostas", "Show replies": "Mostrar respostas", @@ -204,16 +188,12 @@ "This channel does not exist.": "Este canal não existe.", "Could not get channel info.": "Não foi possível obter as informações do canal.", "Could not fetch comments": "Não foi possível obter os comentários", - "View `x` replies.": { - "([^.,0-9]|^)1([^.,0-9]|$)": "Ver `x` respostas.", - "": "Ver `x` respostas." - }, + "View `x` replies..([^.,0-9]|^)1([^.,0-9]|$)": "Ver `x` respostas.", + "View `x` replies..": "Ver `x` respostas.", "`x` ago": "`x` atrás", "Load more": "Carregar mais", - "`x` points.": { - "([^.,0-9]|^)1([^.,0-9]|$)": "'x' pontos.", - "": "'x' pontos." - }, + "`x` points..([^.,0-9]|^)1([^.,0-9]|$)": "'x' pontos.", + "`x` points..": "'x' pontos.", "Could not create mix.": "Não foi possível criar mistura.", "Empty playlist": "Lista de reprodução vazia", "Not a playlist.": "Não é uma lista de reprodução.", @@ -331,34 +311,20 @@ "Yiddish": "Iídiche", "Yoruba": "Ioruba", "Zulu": "Zulu", - "`x` years.": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` anos.", - "": "`x` anos." - }, - "`x` months.": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` meses.", - "": "`x` meses." - }, - "`x` weeks.": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` semanas.", - "": "`x` semanas." - }, - "`x` days.": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` dias.", - "": "`x` dias." - }, - "`x` hours.": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` horas.", - "": "`x` horas." - }, - "`x` minutes.": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` minutos.", - "": "`x` minutos." - }, - "`x` seconds.": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` segundos.", - "": "`x` segundos." - }, + "`x` years..([^.,0-9]|^)1([^.,0-9]|$)": "`x` anos.", + "`x` years..": "`x` anos.", + "`x` months..([^.,0-9]|^)1([^.,0-9]|$)": "`x` meses.", + "`x` months..": "`x` meses.", + "`x` weeks..([^.,0-9]|^)1([^.,0-9]|$)": "`x` semanas.", + "`x` weeks..": "`x` semanas.", + "`x` days..([^.,0-9]|^)1([^.,0-9]|$)": "`x` dias.", + "`x` days..": "`x` dias.", + "`x` hours..([^.,0-9]|^)1([^.,0-9]|$)": "`x` horas.", + "`x` hours..": "`x` horas.", + "`x` minutes..([^.,0-9]|^)1([^.,0-9]|$)": "`x` minutos.", + "`x` minutes..": "`x` minutos.", + "`x` seconds..([^.,0-9]|^)1([^.,0-9]|$)": "`x` segundos.", + "`x` seconds..": "`x` segundos.", "Fallback comments: ": "Comentários alternativos: ", "Popular": "Popular", "Top": "Top", @@ -375,7 +341,7 @@ "Download as: ": "Transferir como: ", "%A %B %-d, %Y": "%A %B %-d, %Y", "(edited)": "(editado)", - "YouTube comment permalink": "Link permanente do comentário do YouTube", + "YouTube comment permalink": "Hiperligação permanente ao comentário do YouTube", "permalink": "ligação permanente", "`x` marked it with a ❤": "`x` foi marcado como ❤", "Audio mode": "Modo de áudio", @@ -384,4 +350,4 @@ "Playlists": "Listas de reprodução", "Community": "Comunidade", "Current version: ": "Versão atual: " -} \ No newline at end of file +} diff --git a/locales/sk.json b/locales/sk.json new file mode 100644 index 00000000..0957cb87 --- /dev/null +++ b/locales/sk.json @@ -0,0 +1,353 @@ +{ + "`x` subscribers.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` subscribers.": "`x` odberateľov.", + "`x` videos.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` videos.": "", + "`x` playlists.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` playlists.": "", + "LIVE": "NAŽIVO", + "Shared `x` ago": "", + "Unsubscribe": "Zrušiť odber", + "Subscribe": "Odoberať", + "View channel on YouTube": "Zobraziť kanál na YouTube", + "View playlist on YouTube": "", + "newest": "najnovšie", + "oldest": "najstaršie", + "popular": "populárne", + "last": "posledné", + "Next page": "Ďalšia strana", + "Previous page": "Predchádzajúca strana", + "Clear watch history?": "Vymazať históriu sledovania?", + "New password": "Nové heslo", + "New passwords must match": "Nové heslá sa musia zhodovať", + "Cannot change password for Google accounts": "Heslo pre účty Google sa nedá zmeniť", + "Authorize token?": "Autorizovať token?", + "Authorize token for `x`?": "", + "Yes": "Áno", + "No": "Nie", + "Import and Export Data": "Import a Export údajov", + "Import": "Import", + "Import Invidious data": "Importovať údaje Invidious", + "Import YouTube subscriptions": "Importovať odbery YouTube", + "Import FreeTube subscriptions (.db)": "Importovať odbery FreeTube (.db)", + "Import NewPipe subscriptions (.json)": "Importovať odbery NewPipe (.json)", + "Import NewPipe data (.zip)": "Importovať údaje NewPipe (.zip)", + "Export": "Export", + "Export subscriptions as OPML": "Exportovať odbery ako OPML", + "Export subscriptions as OPML (for NewPipe & FreeTube)": "Exportovať odbery ako OPML (pre NewPipe a FreeTube)", + "Export data as JSON": "Export údajov ako JSON", + "Delete account?": "Zrušiť účet?", + "History": "História", + "An alternative front-end to YouTube": "Alternatívny front-end pre YouTube", + "JavaScript license information": "Informácie o licencii JavaScript", + "source": "zdroj", + "Log in": "Prihlásiť sa", + "Log in/register": "Prihlásiť sa/Registrovať", + "Log in with Google": "Prihlásiť sa pomocou účtu Google", + "User ID": "ID používateľa", + "Password": "Heslo", + "Time (h:mm:ss):": "Čas (h:mm:ss):", + "Text CAPTCHA": "Textové CAPTCHA", + "Image CAPTCHA": "Obrázkové CAPTCHA", + "Sign In": "Prihlásiť sa", + "Register": "Registrovať", + "E-mail": "E-mail", + "Google verification code": "Overovací kód Google", + "Preferences": "Nastavenia", + "Player preferences": "Nastavenia prehrávača", + "Always loop: ": "Vždy opakovať: ", + "Autoplay: ": "Automatické prehrávanie: ", + "Play next by default: ": "", + "Autoplay next video: ": "Automatické prehrávanie nasledujúceho videa: ", + "Listen by default: ": "Predvolene počúvať: ", + "Proxy videos: ": "Proxy videá: ", + "Default speed: ": "Predvolená rýchlosť: ", + "Preferred video quality: ": "Preferovaná kvalita videa: ", + "Player volume: ": "Hlasitosť prehrávača: ", + "Default comments: ": "Predvolené komentáre: ", + "youtube": "YouTube", + "reddit": "Reddit", + "Default captions: ": "Predvolené titulky: ", + "Fallback captions: ": "Náhradné titulky: ", + "Show related videos: ": "Zobraziť súvisiace videá: ", + "Show annotations by default: ": "Predvolene zobraziť anotácie: ", + "Visual preferences": "Vizuálne nastavenia", + "Player style: ": "Štýl prehrávača: ", + "Dark mode: ": "Tmavý režim: ", + "Theme: ": "Téma: ", + "dark": "tmavá", + "light": "svetlá", + "Thin mode: ": "Tenký režim: ", + "Subscription preferences": "Nastavenia predplatného", + "Show annotations by default for subscribed channels: ": "Predvolene zobraziť anotácie odoberaných kanálov: ", + "Redirect homepage to feed: ": "Presmerovanie domovskej stránky na informačný kanál: ", + "Number of videos shown in feed: ": "Počet videí zobrazených v informačnom kanáli: ", + "Sort videos by: ": "Zoradiť videá podľa: ", + "published": "zverejnené (od najnovších)", + "published - reverse": "zverejnené (od najstarších)", + "alphabetically": "abecedne (A-Z)", + "alphabetically - reverse": "abecedne (Z-A)", + "channel name": "názov kanála (A-Z)", + "channel name - reverse": "názov kanála (Z-A)", + "Only show latest video from channel: ": "Zobraziť iba najnovšie video z kanála: ", + "Only show latest unwatched video from channel: ": "Zobraziť iba najnovšie neprehrané video z kanála: ", + "Only show unwatched: ": "Zobraziť iba neprehrané: ", + "Only show notifications (if there are any): ": "Zobraziť iba upozornenia (ak existujú): ", + "Enable web notifications": "Povoliť webové upozornenia", + "`x` uploaded a video": "`x` nahral(a) video", + "`x` is live": "", + "Data preferences": "", + "Clear watch history": "", + "Import/export data": "", + "Change password": "", + "Manage subscriptions": "", + "Manage tokens": "", + "Watch history": "", + "Delete account": "", + "Administrator preferences": "", + "Default homepage: ": "", + "Feed menu: ": "", + "Top enabled: ": "", + "CAPTCHA enabled: ": "", + "Login enabled: ": "", + "Registration enabled: ": "", + "Report statistics: ": "", + "Save preferences": "", + "Subscription manager": "", + "Token manager": "", + "Token": "", + "`x` subscriptions.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` subscriptions.": "", + "`x` tokens.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` tokens.": "", + "Import/export": "", + "unsubscribe": "", + "revoke": "", + "Subscriptions": "", + "`x` unseen notifications.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` unseen notifications.": "", + "search": "", + "Log out": "", + "Released under the AGPLv3 by Omar Roth.": "", + "Source available here.": "", + "View JavaScript license information.": "", + "View privacy policy.": "", + "Trending": "", + "Public": "", + "Unlisted": "", + "Private": "", + "View all playlists": "", + "Updated `x` ago": "", + "Delete playlist `x`?": "", + "Delete playlist": "", + "Create playlist": "", + "Title": "", + "Playlist privacy": "", + "Editing playlist `x`": "", + "Watch on YouTube": "", + "Hide annotations": "", + "Show annotations": "", + "Genre: ": "", + "License: ": "", + "Family friendly? ": "", + "Wilson score: ": "", + "Engagement: ": "", + "Whitelisted regions: ": "", + "Blacklisted regions: ": "", + "Shared `x`": "", + "`x` views.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` views.": "", + "Premieres in `x`": "", + "Premieres `x`": "", + "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "", + "View YouTube comments": "", + "View more comments on Reddit": "", + "View `x` comments.([^.,0-9]|^)1([^.,0-9]|$)": "", + "View `x` comments.": "", + "View Reddit comments": "", + "Hide replies": "", + "Show replies": "", + "Incorrect password": "", + "Quota exceeded, try again in a few hours": "", + "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "", + "Invalid TFA code": "", + "Login failed. This may be because two-factor authentication is not turned on for your account.": "", + "Wrong answer": "", + "Erroneous CAPTCHA": "", + "CAPTCHA is a required field": "", + "User ID is a required field": "", + "Password is a required field": "", + "Wrong username or password": "", + "Please sign in using 'Log in with Google'": "", + "Password cannot be empty": "", + "Password cannot be longer than 55 characters": "", + "Please log in": "", + "Invidious Private Feed for `x`": "", + "channel:`x`": "", + "Deleted or invalid channel": "", + "This channel does not exist.": "", + "Could not get channel info.": "", + "Could not fetch comments": "", + "View `x` replies.([^.,0-9]|^)1([^.,0-9]|$)": "", + "View `x` replies.": "", + "`x` ago": "", + "Load more": "", + "`x` points.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` points.": "", + "Could not create mix.": "", + "Empty playlist": "", + "Not a playlist.": "", + "Playlist does not exist.": "", + "Could not pull trending pages.": "", + "Hidden field \"challenge\" is a required field": "", + "Hidden field \"token\" is a required field": "", + "Erroneous challenge": "", + "Erroneous token": "", + "No such user": "", + "Token is expired, please try again": "", + "English": "", + "English (auto-generated)": "", + "Afrikaans": "", + "Albanian": "", + "Amharic": "", + "Arabic": "", + "Armenian": "", + "Azerbaijani": "", + "Bangla": "", + "Basque": "", + "Belarusian": "", + "Bosnian": "", + "Bulgarian": "", + "Burmese": "", + "Catalan": "", + "Cebuano": "", + "Chinese (Simplified)": "", + "Chinese (Traditional)": "", + "Corsican": "", + "Croatian": "", + "Czech": "", + "Danish": "", + "Dutch": "", + "Esperanto": "", + "Estonian": "", + "Filipino": "", + "Finnish": "", + "French": "", + "Galician": "", + "Georgian": "", + "German": "", + "Greek": "", + "Gujarati": "", + "Haitian Creole": "", + "Hausa": "", + "Hawaiian": "", + "Hebrew": "", + "Hindi": "", + "Hmong": "", + "Hungarian": "", + "Icelandic": "", + "Igbo": "", + "Indonesian": "", + "Irish": "", + "Italian": "", + "Japanese": "", + "Javanese": "", + "Kannada": "", + "Kazakh": "", + "Khmer": "", + "Korean": "", + "Kurdish": "", + "Kyrgyz": "", + "Lao": "", + "Latin": "", + "Latvian": "", + "Lithuanian": "", + "Luxembourgish": "", + "Macedonian": "", + "Malagasy": "", + "Malay": "", + "Malayalam": "", + "Maltese": "", + "Maori": "", + "Marathi": "", + "Mongolian": "", + "Nepali": "", + "Norwegian Bokmål": "", + "Nyanja": "", + "Pashto": "", + "Persian": "", + "Polish": "", + "Portuguese": "", + "Punjabi": "", + "Romanian": "", + "Russian": "", + "Samoan": "", + "Scottish Gaelic": "", + "Serbian": "", + "Shona": "", + "Sindhi": "", + "Sinhala": "", + "Slovak": "", + "Slovenian": "", + "Somali": "", + "Southern Sotho": "", + "Spanish": "", + "Spanish (Latin America)": "", + "Sundanese": "", + "Swahili": "", + "Swedish": "", + "Tajik": "", + "Tamil": "", + "Telugu": "", + "Thai": "", + "Turkish": "", + "Ukrainian": "", + "Urdu": "", + "Uzbek": "", + "Vietnamese": "", + "Welsh": "", + "Western Frisian": "", + "Xhosa": "", + "Yiddish": "", + "Yoruba": "", + "Zulu": "", + "`x` years.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` years.": "", + "`x` months.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` months.": "", + "`x` weeks.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` weeks.": "", + "`x` days.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` days.": "", + "`x` hours.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` hours.": "", + "`x` minutes.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` minutes.": "", + "`x` seconds.([^.,0-9]|^)1([^.,0-9]|$)": "", + "`x` seconds.": "", + "Fallback comments: ": "", + "Popular": "", + "Top": "", + "About": "", + "Rating: ": "", + "Language: ": "", + "View as playlist": "", + "Default": "", + "Music": "", + "Gaming": "", + "News": "", + "Movies": "", + "Download": "", + "Download as: ": "", + "%A %B %-d, %Y": "", + "(edited)": "", + "YouTube comment permalink": "", + "permalink": "", + "`x` marked it with a ❤": "", + "Audio mode": "", + "Video mode": "", + "Videos": "", + "Playlists": "", + "Community": "", + "Current version: ": "" +} diff --git a/locales/sr_Cyrl.json b/locales/sr_Cyrl.json index 786532df..0ca9a8a0 100644 --- a/locales/sr_Cyrl.json +++ b/locales/sr_Cyrl.json @@ -1,109 +1,109 @@ { - "`x` subscribers.": "", - "`x` videos.": "", - "`x` playlists.": "", - "LIVE": "", - "Shared `x` ago": "", - "Unsubscribe": "", + "`x` subscribers.": "%(count)s пратилац.", + "`x` videos.": "`x` видеа.", + "`x` playlists.": "`x` плејлиста/е.", + "LIVE": "УЖИВО", + "Shared `x` ago": "Објављено пре `x`", + "Unsubscribe": "Прекините праћење", "Subscribe": "Пратите", "View channel on YouTube": "Погледајте канал на YouTube-у", "View playlist on YouTube": "Погледајте плејлисту на YouTube-у", - "newest": "", - "oldest": "", - "popular": "", - "last": "", - "Next page": "", - "Previous page": "", - "Clear watch history?": "", - "New password": "", - "New passwords must match": "", - "Cannot change password for Google accounts": "", - "Authorize token?": "", - "Authorize token for `x`?": "", - "Yes": "", - "No": "", - "Import and Export Data": "", - "Import": "", - "Import Invidious data": "", - "Import YouTube subscriptions": "", - "Import FreeTube subscriptions (.db)": "", - "Import NewPipe subscriptions (.json)": "", - "Import NewPipe data (.zip)": "", - "Export": "", - "Export subscriptions as OPML": "", - "Export subscriptions as OPML (for NewPipe & FreeTube)": "", - "Export data as JSON": "", - "Delete account?": "", - "History": "", - "An alternative front-end to YouTube": "", - "JavaScript license information": "", - "source": "", - "Log in": "", - "Log in/register": "", - "Log in with Google": "", - "User ID": "", - "Password": "", - "Time (h:mm:ss):": "", - "Text CAPTCHA": "", - "Image CAPTCHA": "", - "Sign In": "", - "Register": "", - "E-mail": "", - "Google verification code": "", - "Preferences": "", - "Player preferences": "", - "Always loop: ": "", - "Autoplay: ": "", - "Play next by default: ": "", - "Autoplay next video: ": "", - "Listen by default: ": "", - "Proxy videos: ": "", - "Default speed: ": "", - "Preferred video quality: ": "", - "Player volume: ": "", - "Default comments: ": "", - "youtube": "", - "reddit": "", - "Default captions: ": "", - "Fallback captions: ": "", - "Show related videos: ": "", - "Show annotations by default: ": "", - "Visual preferences": "", - "Player style: ": "", - "Dark mode: ": "", - "Theme: ": "", - "dark": "", - "light": "", - "Thin mode: ": "", - "Subscription preferences": "", - "Show annotations by default for subscribed channels: ": "", - "Redirect homepage to feed: ": "", - "Number of videos shown in feed: ": "", - "Sort videos by: ": "", - "published": "", - "published - reverse": "", - "alphabetically": "", - "alphabetically - reverse": "", - "channel name": "", - "channel name - reverse": "", - "Only show latest video from channel: ": "", - "Only show latest unwatched video from channel: ": "", - "Only show unwatched: ": "", - "Only show notifications (if there are any): ": "", - "Enable web notifications": "", - "`x` uploaded a video": "", - "`x` is live": "", - "Data preferences": "", - "Clear watch history": "", - "Import/export data": "", - "Change password": "", - "Manage subscriptions": "", - "Manage tokens": "", - "Watch history": "", - "Delete account": "", - "Administrator preferences": "", - "Default homepage: ": "", - "Feed menu: ": "", + "newest": "најновије", + "oldest": "најстарије", + "popular": "популарно", + "last": "последње", + "Next page": "Следећа страна", + "Previous page": "Претходна страна", + "Clear watch history?": "Обришите историју прегледања?", + "New password": "Нова лозинка", + "New passwords must match": "Нове лозинке се морају поклапати", + "Cannot change password for Google accounts": "Није могуће променити лозинку за Google налоге", + "Authorize token?": "Овластите токен?", + "Authorize token for `x`?": "Овластите токен за `x`?", + "Yes": "Да", + "No": "Не", + "Import and Export Data": "Увоз и извоз података", + "Import": "Увезите", + "Import Invidious data": "Увезите Invidious податке", + "Import YouTube subscriptions": "Увезите праћења са YouTube-а", + "Import FreeTube subscriptions (.db)": "Увезите праћења са FreeTube-а (.db)", + "Import NewPipe subscriptions (.json)": "Увезите праћења са NewPipe-а (.json)", + "Import NewPipe data (.zip)": "Увезите NewPipe податке (.zip)", + "Export": "Извезите", + "Export subscriptions as OPML": "Извезите праћења у OPML формату", + "Export subscriptions as OPML (for NewPipe & FreeTube)": "Извезите праћења у OPML формату (за NewPipe и FreeTube )", + "Export data as JSON": "Изветизе податке у JSON формату", + "Delete account?": "Избришите налог?", + "History": "Историја", + "An alternative front-end to YouTube": "Алтернативни фронтенд за YouTube", + "JavaScript license information": "Извештај о JavaScript лиценци", + "source": "извор", + "Log in": "Пријавите се", + "Log in/register": "Пријавите се/направите налог", + "Log in with Google": "Пријавите се помоћу Google-а", + "User ID": "ИД корисника", + "Password": "Лозинка", + "Time (h:mm:ss):": "Колико је сати? (ч:мм:сс):", + "Text CAPTCHA": "Текстуална CAPTCHA", + "Image CAPTCHA": "Сликовна CAPTCHA", + "Sign In": "Пријавите се", + "Register": "Направите налог", + "E-mail": "Е-пошта", + "Google verification code": "Google верификациони кôд", + "Preferences": "Подешавања", + "Player preferences": "Подешавања видео плејера", + "Always loop: ": "Увек понављај: ", + "Autoplay: ": "Аутоматско пуштање: ", + "Play next by default: ": "Увек пуштај следеће: ", + "Autoplay next video: ": "Аутоматско пуштање следећег видеа: ", + "Listen by default: ": "Режим слушања као подразумевано: ", + "Proxy videos: ": "Пуштање видеа кроз прокси сервер: ", + "Default speed: ": "Подразумевана брзина репродукције: ", + "Preferred video quality: ": "Претпостављени квалитет видеа: ", + "Player volume: ": "Јачина звука: ", + "Default comments: ": "Подразумевани коментари: ", + "youtube": "са YouTube-а", + "reddit": "са редита", + "Default captions: ": "Подразумевани титлови: ", + "Fallback captions: ": "Алтернативни титлови: ", + "Show related videos: ": "Прикажи сличне видее: ", + "Show annotations by default: ": "Увек приказуј анотације: ", + "Visual preferences": "Подешавања изгледа", + "Player style: ": "Стил плејера: ", + "Dark mode: ": "Тамни режим: ", + "Theme: ": "Тема: ", + "dark": "тамна", + "light": "светла", + "Thin mode: ": "Узани режим: ", + "Subscription preferences": "Подешавања о праћењима", + "Show annotations by default for subscribed channels: ": "Увек приказуј анотације за канале које пратим: ", + "Redirect homepage to feed: ": "Прикажи праћења као почетну страницу: ", + "Number of videos shown in feed: ": "Количина приказаних видеа на доводу: ", + "Sort videos by: ": "Сортирај према: ", + "published": "датуму објављивања", + "published - reverse": "датуму објављивања - обрнуто", + "alphabetically": "алфабету", + "alphabetically - reverse": "алфабету - обрнуто", + "channel name": "називу канала", + "channel name - reverse": "називу канала - обрнуто", + "Only show latest video from channel: ": "Прикажи само најновији видео са канала: ", + "Only show latest unwatched video from channel: ": "Прикажи само најновији негледани видео са канала: ", + "Only show unwatched: ": "Прикажи само негледано: ", + "Only show notifications (if there are any): ": "Прикажи само обавештења (ако их има): ", + "Enable web notifications": "Укључи обавештења преко претраживача", + "`x` uploaded a video": "`x`је објавио/ла видео", + "`x` is live": "`x` емитује уживо", + "Data preferences": "Подешавања о подацима", + "Clear watch history": "Обришите историју прегледања", + "Import/export data": "Увезите или извезите податке", + "Change password": "Промените лозинку", + "Manage subscriptions": "Управљајте праћењима", + "Manage tokens": "Управљајте токенима", + "Watch history": "Историја прегледања", + "Delete account": "Избришите налог", + "Administrator preferences": "Подешавања администратора", + "Default homepage: ": "Подразумевана главна страница: ", + "Feed menu: ": "Мени довода: ", "Top enabled: ": "", "CAPTCHA enabled: ": "", "Login enabled: ": "", @@ -333,4 +333,4 @@ "Playlists": "", "Community": "", "Current version: ": "Тренутна верзија: " -} \ No newline at end of file +} diff --git a/locales/sv-SE.json b/locales/sv-SE.json index 14e7d53e..fef5316f 100644 --- a/locales/sv-SE.json +++ b/locales/sv-SE.json @@ -195,112 +195,112 @@ "Erroneous token": "Felaktig token", "No such user": "Ogiltig användare", "Token is expired, please try again": "Token föråldrad, försök igen", - "English": "", - "English (auto-generated)": "English (auto-genererat)", - "Afrikaans": "", - "Albanian": "", - "Amharic": "", - "Arabic": "", - "Armenian": "", - "Azerbaijani": "", - "Bangla": "", - "Basque": "", - "Belarusian": "", - "Bosnian": "", - "Bulgarian": "", - "Burmese": "", - "Catalan": "", - "Cebuano": "", - "Chinese (Simplified)": "", - "Chinese (Traditional)": "", - "Corsican": "", - "Croatian": "", - "Czech": "", - "Danish": "", - "Dutch": "", - "Esperanto": "", - "Estonian": "", - "Filipino": "", - "Finnish": "", - "French": "", - "Galician": "", - "Georgian": "", - "German": "", - "Greek": "", - "Gujarati": "", - "Haitian Creole": "", - "Hausa": "", - "Hawaiian": "", - "Hebrew": "", - "Hindi": "", - "Hmong": "", - "Hungarian": "", - "Icelandic": "", - "Igbo": "", - "Indonesian": "", - "Irish": "", - "Italian": "", - "Japanese": "", - "Javanese": "", - "Kannada": "", - "Kazakh": "", - "Khmer": "", - "Korean": "", - "Kurdish": "", - "Kyrgyz": "", - "Lao": "", - "Latin": "", - "Latvian": "", - "Lithuanian": "", - "Luxembourgish": "", - "Macedonian": "", - "Malagasy": "", - "Malay": "", - "Malayalam": "", - "Maltese": "", - "Maori": "", - "Marathi": "", - "Mongolian": "", - "Nepali": "", - "Norwegian Bokmål": "", - "Nyanja": "", - "Pashto": "", - "Persian": "", - "Polish": "", - "Portuguese": "", - "Punjabi": "", - "Romanian": "", - "Russian": "", - "Samoan": "", - "Scottish Gaelic": "", - "Serbian": "", - "Shona": "", - "Sindhi": "", - "Sinhala": "", - "Slovak": "", - "Slovenian": "", - "Somali": "", - "Southern Sotho": "", - "Spanish": "", - "Spanish (Latin America)": "", - "Sundanese": "", - "Swahili": "", - "Swedish": "", - "Tajik": "", - "Tamil": "", - "Telugu": "", - "Thai": "", - "Turkish": "", - "Ukrainian": "", - "Urdu": "", - "Uzbek": "", - "Vietnamese": "", - "Welsh": "", - "Western Frisian": "", - "Xhosa": "", - "Yiddish": "", - "Yoruba": "", - "Zulu": "", + "English": "Engelska", + "English (auto-generated)": "Engelska (auto-genererat)", + "Afrikaans": "Afrikanska", + "Albanian": "Albanska", + "Amharic": "Amhariska", + "Arabic": "Arabiska", + "Armenian": "Armeniska", + "Azerbaijani": "Azerbajdzjanska", + "Bangla": "Bengaliska", + "Basque": "Baskiska", + "Belarusian": "Vitryska", + "Bosnian": "Bosniska", + "Bulgarian": "Bulgariska", + "Burmese": "Burmesiska", + "Catalan": "Katalanska", + "Cebuano": "Cebuano", + "Chinese (Simplified)": "Kinesiska (Förenklad)", + "Chinese (Traditional)": "Kinesiska (Traditionell)", + "Corsican": "Korsikanska", + "Croatian": "Kroatiska", + "Czech": "Tjeckiska", + "Danish": "Danska", + "Dutch": "Nederländska", + "Esperanto": "Esperanto", + "Estonian": "Estniska", + "Filipino": "Filipino", + "Finnish": "Finska", + "French": "Franska", + "Galician": "Galiciska", + "Georgian": "Georgiska", + "German": "Tyska", + "Greek": "Grekiska", + "Gujarati": "Gujarati", + "Haitian Creole": "Haitisk Kreol", + "Hausa": "Hausa", + "Hawaiian": "Hawaiiska", + "Hebrew": "Hebreiska", + "Hindi": "Hindi", + "Hmong": "Hmong-mienspråk", + "Hungarian": "Ungerska", + "Icelandic": "Isländska", + "Igbo": "Igbo", + "Indonesian": "Indonesiska", + "Irish": "Irländska", + "Italian": "Italienska", + "Japanese": "Japanska", + "Javanese": "Javanesiska", + "Kannada": "Kanaresiska", + "Kazakh": "Kazakiska", + "Khmer": "Kambodjanska", + "Korean": "Koreanska", + "Kurdish": "Kurdiska", + "Kyrgyz": "Kirgiziska", + "Lao": "Laotiska", + "Latin": "Latin", + "Latvian": "Lettiska", + "Lithuanian": "Litauiska", + "Luxembourgish": "Luxemburgska", + "Macedonian": "Makedonska", + "Malagasy": "Malagassiska", + "Malay": "Malajiska", + "Malayalam": "Malayalam", + "Maltese": "Maltesiska", + "Maori": "Maori", + "Marathi": "Marathi", + "Mongolian": "Mongoliska", + "Nepali": "Nepali", + "Norwegian Bokmål": "Norska Bokmål", + "Nyanja": "Nyanja", + "Pashto": "Pashto", + "Persian": "Persiska", + "Polish": "Polska", + "Portuguese": "Portugisiska", + "Punjabi": "Punjabi", + "Romanian": "Rumänska", + "Russian": "Ryska", + "Samoan": "Samoanska", + "Scottish Gaelic": "Skotsk gäliska", + "Serbian": "Serbiska", + "Shona": "Shona", + "Sindhi": "Sindhi", + "Sinhala": "Singalesiska", + "Slovak": "Slovakiska", + "Slovenian": "Slovenska", + "Somali": "Somaliska", + "Southern Sotho": "Sydsotho", + "Spanish": "Spanska", + "Spanish (Latin America)": "Spanska (Latin Amerikansk)", + "Sundanese": "Sundanesiska", + "Swahili": "Swahili", + "Swedish": "Svenska", + "Tajik": "Tadzjikiska", + "Tamil": "Tamil", + "Telugu": "Telugu", + "Thai": "Thailändska", + "Turkish": "Turkiska", + "Ukrainian": "Ukrainska", + "Urdu": "Urdu", + "Uzbek": "Uzbekiska", + "Vietnamese": "Vietnamesiska", + "Welsh": "Walesiska", + "Western Frisian": "Västfrisiska", + "Xhosa": "Xhosa", + "Yiddish": "Jiddisch", + "Yoruba": "Yoruba", + "Zulu": "Zulu", "`x` years": "`x` år", "`x` months": "`x` månader", "`x` weeks": "`x` veckor", @@ -322,7 +322,7 @@ "Movies": "Filmer", "Download": "Ladda ned", "Download as: ": "Ladda ned som: ", - "%A %B %-d, %Y": "", + "%A %B %-d, %Y": "%A %B %-d, %Y", "(edited)": "(redigerad)", "YouTube comment permalink": "Permanent YouTube-länk till innehållet", "permalink": "permalänk", @@ -333,4 +333,4 @@ "Playlists": "Spellistor", "Community": "Gemenskap", "Current version: ": "Nuvarande version: " -} \ No newline at end of file +} diff --git a/locales/tr.json b/locales/tr.json index 652dff6d..831ecc07 100644 --- a/locales/tr.json +++ b/locales/tr.json @@ -334,11 +334,11 @@ "(edited)": "(düzenlendi)", "YouTube comment permalink": "YouTube yorumu kalıcı linki", "permalink": "kalıcı link", - "`x` marked it with a ❤": "`x` ❤ ile işaretlendi", + "`x` marked it with a ❤": "`x` ❤ ile işaretledi", "Audio mode": "Ses modu", "Video mode": "Video modu", "Videos": "Videolar", "Playlists": "Oynatma listeleri", "Community": "Topluluk", "Current version: ": "Şu anki sürüm: " -} \ No newline at end of file +} diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 288f127d..d7f12975 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -11,7 +11,7 @@ "newest": "最新", "oldest": "最老", "popular": "时下流行", - "last": "last", + "last": "", "Next page": "下一页", "Previous page": "上一页", "Clear watch history?": "清除观看历史?", @@ -52,44 +52,44 @@ "Google verification code": "Google 验证代码", "Preferences": "偏好设置", "Player preferences": "播放器偏好设置", - "Always loop: ": "循环:", - "Autoplay: ": "自动播放:", - "Play next by default: ": "默认自动播放下一个视频:", - "Autoplay next video: ": "自动播放下一个视频:", - "Listen by default: ": "默认只聆听声音:", - "Proxy videos: ": "代理视频?", - "Default speed: ": "默认速度:", - "Preferred video quality: ": "视频质量偏好:", - "Player volume: ": "播放器音量:", - "Default comments: ": "默认评论源:", + "Always loop: ": "始终循环: ", + "Autoplay: ": "自动播放: ", + "Play next by default: ": "默认自动播放下一个视频: ", + "Autoplay next video: ": "自动播放下一个视频: ", + "Listen by default: ": "默认只听声音: ", + "Proxy videos: ": "是否代理视频: ", + "Default speed: ": "默认速度: ", + "Preferred video quality: ": "视频质量偏好: ", + "Player volume: ": "播放器音量: ", + "Default comments: ": "默认评论源: ", "youtube": "YouTube", "reddit": "Reddit", - "Default captions: ": "默认字幕语言:", - "Fallback captions: ": "后备字幕语言:", - "Show related videos: ": "显示相关视频?", - "Show annotations by default: ": "默认显示视频注释?", + "Default captions: ": "默认字幕语言: ", + "Fallback captions: ": "后备字幕语言: ", + "Show related videos: ": "是否显示相关视频: ", + "Show annotations by default: ": "是否默认显示视频注释: ", "Visual preferences": "视觉选项", - "Player style: ": "播放器样式:", - "Dark mode: ": "暗色模式:", - "Theme: ": "主题", + "Player style: ": "播放器样式: ", + "Dark mode: ": "深色模式: ", + "Theme: ": "主题: ", "dark": "暗色", "light": "亮色", - "Thin mode: ": "窄页模式:", + "Thin mode: ": "窄页模式: ", "Subscription preferences": "订阅设置", - "Show annotations by default for subscribed channels: ": "在订阅频道的视频默认显示注释?", + "Show annotations by default for subscribed channels: ": "默认情况下显示已订阅频道的注释: ", "Redirect homepage to feed: ": "跳转主页到 feed: ", - "Number of videos shown in feed: ": "Feed 中显示的视频数量:", - "Sort videos by: ": "视频排序方式:", + "Number of videos shown in feed: ": "Feed 中显示的视频数量: ", + "Sort videos by: ": "视频排序方式: ", "published": "发布时间", "published - reverse": "发布时间(反向)", "alphabetically": "字母序", "alphabetically - reverse": "字母序(反向)", "channel name": "频道名称", "channel name - reverse": "频道名称(反向)", - "Only show latest video from channel: ": "只显示订阅频道的最新一条视频:", - "Only show latest unwatched video from channel: ": "只显示订阅频道的最新未看过视频:", - "Only show unwatched: ": "只显示未看过的视频:", - "Only show notifications (if there are any): ": "只显示通知(如有):", + "Only show latest video from channel: ": "只显示频道的最新视频: ", + "Only show latest unwatched video from channel: ": "只显示频道的最新未看过视频: ", + "Only show unwatched: ": "只显示未看过的视频: ", + "Only show notifications (if there are any): ": "只显示通知 (如果有的话): ", "Enable web notifications": "启用浏览器通知", "`x` uploaded a video": "`x` 上传了视频", "`x` is live": "`x` 正在直播", @@ -102,13 +102,13 @@ "Watch history": "观看历史", "Delete account": "删除账户", "Administrator preferences": "管理员选项", - "Default homepage: ": "默认主页:", - "Feed menu: ": "Feed 菜单:", - "Top enabled: ": "启用“热门视频”页?", - "CAPTCHA enabled: ": "启用验证码?", - "Login enabled: ": "启用登录?", - "Registration enabled: ": "启用注册?", - "Report statistics: ": "报告统计信息?", + "Default homepage: ": "默认主页: ", + "Feed menu: ": "Feed 菜单: ", + "Top enabled: ": "是否启用“热门视频”页: ", + "CAPTCHA enabled: ": "是否启用验证码: ", + "Login enabled: ": "是否启用登录: ", + "Registration enabled: ": "是否启用注册: ", + "Report statistics: ": "是否报告统计信息: ", "Save preferences": "保存选项", "Subscription manager": "订阅管理器", "Token manager": "令牌管理器", @@ -141,13 +141,13 @@ "Watch on YouTube": "在 YouTube 观看", "Hide annotations": "隐藏注释", "Show annotations": "显示注释", - "Genre: ": "风格:", - "License: ": "协议:", - "Family friendly? ": "家庭友好?", - "Wilson score: ": "威尔逊得分:", - "Engagement: ": "参与度:", - "Whitelisted regions: ": "白名单区域:", - "Blacklisted regions: ": "黑名单区域:", + "Genre: ": "风格: ", + "License: ": "许可: ", + "Family friendly? ": "家庭友好? ", + "Wilson score: ": "威尔逊得分: ", + "Engagement: ": "参与度: ", + "Whitelisted regions: ": "白名单地区: ", + "Blacklisted regions: ": "黑名单地区: ", "Shared `x`": "`x`发布", "`x` views": "`x` 播放", "Premieres in `x`": "首映于 `x` 后", @@ -308,12 +308,12 @@ "`x` hours": "`x` 小时", "`x` minutes": "`x` 分钟", "`x` seconds": "`x` 秒", - "Fallback comments: ": "后备评论:", + "Fallback comments: ": "后备评论: ", "Popular": "热门频道", "Top": "热门视频", "About": "关于", - "Rating: ": "评分:", - "Language: ": "语言:", + "Rating: ": "评分: ", + "Language: ": "语言: ", "View as playlist": "作为播放列表查看", "Default": "默认", "Music": "音乐", @@ -321,7 +321,7 @@ "News": "新闻", "Movies": "电影", "Download": "下载", - "Download as: ": "下载为:", + "Download as: ": "下载为: ", "%A %B %-d, %Y": "%Y年%-m月%-d日 %a", "(edited)": "(已编辑)", "YouTube comment permalink": "YouTube 评论永久链接", @@ -332,5 +332,5 @@ "Videos": "视频", "Playlists": "播放列表", "Community": "社区", - "Current version: ": "当前版本:" -} \ No newline at end of file + "Current version: ": "当前版本: " +} diff --git a/locales/zh-TW.json b/locales/zh-TW.json index a8111750..6b40db55 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -1,12 +1,8 @@ { - "`x` subscribers": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` 個訂閱者", - "": "`x` 個訂閱者。" - }, - "`x` videos": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` 部影片", - "": "`x` 部影片。" - }, + "`x` subscribers.([^.,0-9]|^)1([^.,0-9]|$)": "`x` 個訂閱者", + "`x` subscribers.": "`x` 個訂閱者", + "`x` videos.([^.,0-9]|^)1([^.,0-9]|$)": "`x` 部影片", + "`x` videos.": "`x` 部影片", "`x` playlists": "`x` 播放清單", "LIVE": "直播", "Shared `x` ago": "`x` 前分享", @@ -119,22 +115,16 @@ "Subscription manager": "訂閱管理員", "Token manager": "Token 管理員", "Token": "Token", - "`x` subscriptions": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` 個訂閱", - "": "`x` 個訂閱。" - }, - "`x` tokens": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` token", - "": "`x` tokens。" - }, + "`x` subscriptions.([^.,0-9]|^)1([^.,0-9]|$)": "`x` 個訂閱", + "`x` subscriptions.": "`x` 個訂閱", + "`x` tokens.([^.,0-9]|^)1([^.,0-9]|$)": "`x` token", + "`x` tokens.": "`x` 個存取金鑰", "Import/export": "匯入/匯出", "unsubscribe": "取消訂閱", "revoke": "撤銷", "Subscriptions": "訂閱", - "`x` unseen notifications": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` 個未讀的通知", - "": "`x` 個未讀的通知。" - }, + "`x` unseen notifications.([^.,0-9]|^)1([^.,0-9]|$)": "`x` 個未讀的通知", + "`x` unseen notifications.": "`x` 個未讀的通知", "search": "搜尋", "Log out": "登出", "Released under the AGPLv3 by Omar Roth.": "Omar Roth 以 AGPLv3 釋出。", @@ -164,10 +154,8 @@ "Whitelisted regions: ": "白名單區域: ", "Blacklisted regions: ": "黑名單區域: ", "Shared `x`": "`x` 發佈", - "`x` views": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` 次檢視", - "": "`x` 次檢視。" - }, + "`x` views.([^.,0-9]|^)1([^.,0-9]|$)": "`x` 次檢視", + "`x` views.": "`x` 次檢視", "Premieres in `x`": "首映於 `x`", "Premieres `x`": "首映於 `x`", "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "嗨!看來您將 JavaScript 關閉了。點擊這裡以檢視留言,請注意,它們可能需要比較長的時間載入。", @@ -198,16 +186,12 @@ "This channel does not exist.": "此頻道不存在。", "Could not get channel info.": "無法取得頻道資訊。", "Could not fetch comments": "無法擷取留言", - "View `x` replies": { - "([^.,0-9]|^)1([^.,0-9]|$)": "檢視 `x` 則回覆", - "": "檢視 `x` 則回覆。" - }, + "View `x` replies.([^.,0-9]|^)1([^.,0-9]|$)": "檢視 `x` 則回覆", + "View `x` replies.": "檢視 `x` 則回覆", "`x` ago": "`x` 以前", "Load more": "載入更多", - "`x` points": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` 點", - "": "`x` 點。" - }, + "`x` points.([^.,0-9]|^)1([^.,0-9]|$)": "`x` 點", + "`x` points.": "`x` 點", "Could not create mix.": "無法建立混合。", "Empty playlist": "空的播放清單", "Not a playlist.": "不是播放清單。", @@ -325,34 +309,20 @@ "Yiddish": "意第緒語", "Yoruba": "約魯巴語", "Zulu": "祖魯語", - "`x` years": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` 年", - "": "`x` 年。" - }, - "`x` months": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` 月", - "": "`x` 月。" - }, - "`x` weeks": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` 週", - "": "`x` 週。" - }, - "`x` days": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` 天", - "": "`x` 天。" - }, - "`x` hours": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` 小時", - "": "`x` 小時。" - }, - "`x` minutes": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` 天", - "": "`x` 分鐘。" - }, - "`x` seconds": { - "([^.,0-9]|^)1([^.,0-9]|$)": "`x` 秒", - "": "`x` 秒。" - }, + "`x` years.([^.,0-9]|^)1([^.,0-9]|$)": "`x` 年", + "`x` years.": "`x` 年", + "`x` months.([^.,0-9]|^)1([^.,0-9]|$)": "`x` 月", + "`x` months.": "`x` 月", + "`x` weeks.([^.,0-9]|^)1([^.,0-9]|$)": "`x` 週", + "`x` weeks.": "`x` 週", + "`x` days.([^.,0-9]|^)1([^.,0-9]|$)": "`x` 天", + "`x` days.": "`x` 天", + "`x` hours.([^.,0-9]|^)1([^.,0-9]|$)": "`x` 小時", + "`x` hours.": "`x` 小時", + "`x` minutes.([^.,0-9]|^)1([^.,0-9]|$)": "`x` 天", + "`x` minutes.": "`x` 分鐘", + "`x` seconds.([^.,0-9]|^)1([^.,0-9]|$)": "`x` 秒", + "`x` seconds.": "`x` 秒", "Fallback comments: ": "汰退留言: ", "Popular": "熱門頻道", "Top": "熱門影片", @@ -378,4 +348,4 @@ "Playlists": "播放清單", "Community": "社群", "Current version: ": "目前版本: " -} \ No newline at end of file +} From c3ed1ad040264de74b0787952e4f976c9b32a9b8 Mon Sep 17 00:00:00 2001 From: TheFrenchGhosty Date: Sat, 23 Jan 2021 18:22:05 +0100 Subject: [PATCH 0299/2930] Change some stuff done to the french translation in #1696 --- locales/fr.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index 664e25f5..d6bdc05a 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -48,7 +48,7 @@ "Image CAPTCHA": "CAPTCHA Image", "Sign In": "Se connecter", "Register": "S'inscrire", - "E-mail": "Courriel", + "E-mail": "E-mail", "Google verification code": "Code de vérification Google", "Preferences": "Préférences", "Player preferences": "Préférences du lecteur", @@ -170,7 +170,7 @@ "User ID is a required field": "Veuillez entrer un Identifiant Utilisateur", "Password is a required field": "Veuillez entrer un Mot de passe", "Wrong username or password": "Nom d'utilisateur ou mot de passe invalide", - "Please sign in using 'Log in with Google'": "Veuillez vous connecter en utilisant « Se connecter avec Google »", + "Please sign in using 'Log in with Google'": "Veuillez vous connecter en utilisant \"Se connecter avec Google\"", "Password cannot be empty": "Le mot de passe ne peut pas être vide", "Password cannot be longer than 55 characters": "Le mot de passe ne doit pas comporter plus de 55 caractères", "Please log in": "Veuillez vous connecter", @@ -189,7 +189,7 @@ "Not a playlist.": "La liste de lecture est invalide.", "Playlist does not exist.": "La liste de lecture n'existe pas.", "Could not pull trending pages.": "Impossible de charger les pages de tendances.", - "Hidden field \"challenge\" is a required field": "Le champ masqué « challenge » est un champ obligatoire", + "Hidden field \"challenge\" is a required field": "Le champ masqué \"challenge\" est un champ obligatoire", "Hidden field \"token\" is a required field": "Le champ caché « token » est requis", "Erroneous challenge": "Challenge invalide", "Erroneous token": "Token invalide", From f1a7ee997b5f6bc52388473dcd1b8043c5ceacff Mon Sep 17 00:00:00 2001 From: saltycrys <73420320+saltycrys@users.noreply.github.com> Date: Sat, 23 Jan 2021 18:58:13 +0100 Subject: [PATCH 0300/2930] Add config environment variables The config file can now be specified with `INVIDIOUS_CONFIG_FILE`. A YAML formatted string can still be passed with `INVIDIOUS_CONFIG`, replacing the config file. Additionally all options can now be specified as environment variables. The syntax for variable names is `INVIDIOUS_` followed by the option name in upper case. The values are parsed as YAML. These new env vars only update the provided main configuration, but it is possible to point the config file at the example config and then use env vars for all config options: ``` INVIDIOUS_CONFIG_FILE=./config/config.example.yml \ INVIDIOUS_CHANNEL_THREADS=10 \ ./invidious ``` --- src/invidious.cr | 7 ++-- src/invidious/helpers/helpers.cr | 57 ++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index deb24ac3..1d86bb11 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -30,11 +30,8 @@ require "./invidious/*" require "./invidious/routes/**" require "./invidious/jobs/**" -ENV_CONFIG_NAME = "INVIDIOUS_CONFIG" - -CONFIG_STR = ENV.has_key?(ENV_CONFIG_NAME) ? ENV.fetch(ENV_CONFIG_NAME) : File.read("config/config.yml") -CONFIG = Config.from_yaml(CONFIG_STR) -HMAC_KEY = CONFIG.hmac_key || Random::Secure.hex(32) +CONFIG = Config.load +HMAC_KEY = CONFIG.hmac_key || Random::Secure.hex(32) PG_URL = URI.new( scheme: "postgres", diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr index 1f56ec92..bcd78699 100644 --- a/src/invidious/helpers/helpers.cr +++ b/src/invidious/helpers/helpers.cr @@ -115,6 +115,63 @@ class Config 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 %} + + return config + end end struct DBConfig From b45f371911502d6687fc0402139af5268ee5b13f Mon Sep 17 00:00:00 2001 From: saltycrys <73420320+saltycrys@users.noreply.github.com> Date: Sat, 23 Jan 2021 19:39:04 +0100 Subject: [PATCH 0301/2930] Make config a constant Instead of passing around `config` there is now the global `CONFIG`. --- src/invidious.cr | 32 +++++++++----------- src/invidious/helpers/errors.cr | 32 ++++++++++---------- src/invidious/helpers/utils.cr | 12 ++++---- src/invidious/jobs/bypass_captcha_job.cr | 29 ++++++++---------- src/invidious/jobs/refresh_channels_job.cr | 7 ++--- src/invidious/jobs/refresh_feeds_job.cr | 5 ++- src/invidious/jobs/statistics_refresh_job.cr | 5 ++- src/invidious/jobs/subscribe_to_feeds_job.cr | 9 +++--- src/invidious/routes/base_route.cr | 4 --- src/invidious/routes/login.cr | 24 +++++++-------- src/invidious/routes/user_preferences.cr | 31 +++++++++---------- src/invidious/routing.cr | 4 +-- src/invidious/views/preferences.ecr | 14 ++++----- src/invidious/views/template.ecr | 2 +- 14 files changed, 97 insertions(+), 113 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index 1d86bb11..66a88f56 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -49,7 +49,7 @@ PUBSUB_URL = URI.parse("https://pubsubhubbub.appspot.com") REDDIT_URL = URI.parse("https://www.reddit.com") TEXTCAPTCHA_URL = URI.parse("https://textcaptcha.com") YT_URL = URI.parse("https://www.youtube.com") -HOST_URL = make_host_url(CONFIG, Kemal.config) +HOST_URL = make_host_url(Kemal.config) CHARS_SAFE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" TEST_IDS = {"AgbeGFYluEA", "BaW_jenozKc", "a9LDPn-MO4I", "ddFvjfvPnqk", "iqKdEhx-dD4"} @@ -142,8 +142,6 @@ end OUTPUT = CONFIG.output.upcase == "STDOUT" ? STDOUT : File.open(CONFIG.output, mode: "a") LOGGER = Invidious::LogHandler.new(OUTPUT, CONFIG.log_level) -config = CONFIG - # Check table integrity if CONFIG.check_tables check_enum(PG_DB, "privacy", PlaylistPrivacy) @@ -164,28 +162,28 @@ end # Start jobs -Invidious::Jobs.register Invidious::Jobs::RefreshChannelsJob.new(PG_DB, config) -Invidious::Jobs.register Invidious::Jobs::RefreshFeedsJob.new(PG_DB, config) +Invidious::Jobs.register Invidious::Jobs::RefreshChannelsJob.new(PG_DB) +Invidious::Jobs.register Invidious::Jobs::RefreshFeedsJob.new(PG_DB) DECRYPT_FUNCTION = DecryptFunction.new(CONFIG.decrypt_polling) -if config.decrypt_polling +if CONFIG.decrypt_polling Invidious::Jobs.register Invidious::Jobs::UpdateDecryptFunctionJob.new end -if config.statistics_enabled - Invidious::Jobs.register Invidious::Jobs::StatisticsRefreshJob.new(PG_DB, config, SOFTWARE) +if CONFIG.statistics_enabled + Invidious::Jobs.register Invidious::Jobs::StatisticsRefreshJob.new(PG_DB, SOFTWARE) end -if (config.use_pubsub_feeds.is_a?(Bool) && config.use_pubsub_feeds.as(Bool)) || (config.use_pubsub_feeds.is_a?(Int32) && config.use_pubsub_feeds.as(Int32) > 0) - Invidious::Jobs.register Invidious::Jobs::SubscribeToFeedsJob.new(PG_DB, config, HMAC_KEY) +if (CONFIG.use_pubsub_feeds.is_a?(Bool) && CONFIG.use_pubsub_feeds.as(Bool)) || (CONFIG.use_pubsub_feeds.is_a?(Int32) && CONFIG.use_pubsub_feeds.as(Int32) > 0) + Invidious::Jobs.register Invidious::Jobs::SubscribeToFeedsJob.new(PG_DB, HMAC_KEY) end -if config.popular_enabled +if CONFIG.popular_enabled Invidious::Jobs.register Invidious::Jobs::PullPopularVideosJob.new(PG_DB) end -if config.captcha_key - Invidious::Jobs.register Invidious::Jobs::BypassCaptchaJob.new(config) +if CONFIG.captcha_key + Invidious::Jobs.register Invidious::Jobs::BypassCaptchaJob.new end connection_channel = Channel({Bool, Channel(PQ::Notification)}).new(32) @@ -216,7 +214,7 @@ before_all do |env| env.response.headers["Content-Security-Policy"] = "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; manifest-src 'self'; media-src 'self' blob:#{extra_media_csp}" env.response.headers["Referrer-Policy"] = "same-origin" - if (Kemal.config.ssl || config.https_only) && config.hsts + if (Kemal.config.ssl || CONFIG.https_only) && CONFIG.hsts env.response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains; preload" end @@ -1161,7 +1159,7 @@ end get "/feed/popular" do |env| locale = LOCALES[env.get("preferences").as(Preferences).locale]? - if config.popular_enabled + if CONFIG.popular_enabled templated "popular" else message = translate(locale, "The Popular feed has been disabled by the administrator.") @@ -1819,7 +1817,7 @@ get "/api/v1/stats" do |env| locale = LOCALES[env.get("preferences").as(Preferences).locale]? env.response.content_type = "application/json" - if !config.statistics_enabled + if !CONFIG.statistics_enabled next error_json(400, "Statistics are not enabled.") end @@ -2229,7 +2227,7 @@ get "/api/v1/popular" do |env| env.response.content_type = "application/json" - if !config.popular_enabled + if !CONFIG.popular_enabled error_message = {"error" => "Administrator has disabled this endpoint."}.to_json env.response.status_code = 400 next error_message diff --git a/src/invidious/helpers/errors.cr b/src/invidious/helpers/errors.cr index 2c62d44b..68ced430 100644 --- a/src/invidious/helpers/errors.cr +++ b/src/invidious/helpers/errors.cr @@ -7,7 +7,7 @@ class InfoException < Exception end macro error_template(*args) - error_template_helper(env, config, locale, {{*args}}) + error_template_helper(env, locale, {{*args}}) end def github_details(summary : String, content : String) @@ -22,9 +22,9 @@ def github_details(summary : String, content : String) return HTML.escape(details) end -def error_template_helper(env : HTTP::Server::Context, config : Config, locale : Hash(String, JSON::Any) | Nil, status_code : Int32, exception : Exception) +def error_template_helper(env : HTTP::Server::Context, locale : Hash(String, JSON::Any) | Nil, status_code : Int32, exception : Exception) if exception.is_a?(InfoException) - return error_template_helper(env, config, locale, status_code, exception.message || "") + return error_template_helper(env, locale, status_code, exception.message || "") end env.response.content_type = "text/html" env.response.status_code = status_code @@ -43,7 +43,7 @@ def error_template_helper(env : HTTP::Server::Context, config : Config, locale : return templated "error" end -def error_template_helper(env : HTTP::Server::Context, config : Config, locale : Hash(String, JSON::Any) | Nil, status_code : Int32, message : String) +def error_template_helper(env : HTTP::Server::Context, locale : Hash(String, JSON::Any) | Nil, status_code : Int32, message : String) env.response.content_type = "text/html" env.response.status_code = status_code error_message = translate(locale, message) @@ -51,31 +51,31 @@ def error_template_helper(env : HTTP::Server::Context, config : Config, locale : end macro error_atom(*args) - error_atom_helper(env, config, locale, {{*args}}) + error_atom_helper(env, locale, {{*args}}) end -def error_atom_helper(env : HTTP::Server::Context, config : Config, locale : Hash(String, JSON::Any) | Nil, status_code : Int32, exception : Exception) +def error_atom_helper(env : HTTP::Server::Context, locale : Hash(String, JSON::Any) | Nil, status_code : Int32, exception : Exception) if exception.is_a?(InfoException) - return error_atom_helper(env, config, locale, status_code, exception.message || "") + return error_atom_helper(env, locale, status_code, exception.message || "") end env.response.content_type = "application/atom+xml" env.response.status_code = status_code return "#{exception.inspect_with_backtrace}" end -def error_atom_helper(env : HTTP::Server::Context, config : Config, locale : Hash(String, JSON::Any) | Nil, status_code : Int32, message : String) +def error_atom_helper(env : HTTP::Server::Context, locale : Hash(String, JSON::Any) | Nil, status_code : Int32, message : String) env.response.content_type = "application/atom+xml" env.response.status_code = status_code return "#{message}" end macro error_json(*args) - error_json_helper(env, config, locale, {{*args}}) + error_json_helper(env, locale, {{*args}}) end -def error_json_helper(env : HTTP::Server::Context, config : Config, locale : Hash(String, JSON::Any) | Nil, status_code : Int32, exception : Exception, additional_fields : Hash(String, Object) | Nil) +def error_json_helper(env : HTTP::Server::Context, locale : Hash(String, JSON::Any) | Nil, status_code : Int32, exception : Exception, additional_fields : Hash(String, Object) | Nil) if exception.is_a?(InfoException) - return error_json_helper(env, config, locale, status_code, exception.message || "", additional_fields) + return error_json_helper(env, locale, status_code, exception.message || "", additional_fields) end env.response.content_type = "application/json" env.response.status_code = status_code @@ -86,11 +86,11 @@ def error_json_helper(env : HTTP::Server::Context, config : Config, locale : Has return error_message.to_json end -def error_json_helper(env : HTTP::Server::Context, config : Config, locale : Hash(String, JSON::Any) | Nil, status_code : Int32, exception : Exception) - return error_json_helper(env, config, locale, status_code, exception, nil) +def error_json_helper(env : HTTP::Server::Context, locale : Hash(String, JSON::Any) | Nil, status_code : Int32, exception : Exception) + return error_json_helper(env, locale, status_code, exception, nil) end -def error_json_helper(env : HTTP::Server::Context, config : Config, locale : Hash(String, JSON::Any) | Nil, status_code : Int32, message : String, additional_fields : Hash(String, Object) | Nil) +def error_json_helper(env : HTTP::Server::Context, locale : Hash(String, JSON::Any) | Nil, status_code : Int32, message : String, additional_fields : Hash(String, Object) | Nil) env.response.content_type = "application/json" env.response.status_code = status_code error_message = {"error" => message} @@ -100,6 +100,6 @@ def error_json_helper(env : HTTP::Server::Context, config : Config, locale : Has return error_message.to_json end -def error_json_helper(env : HTTP::Server::Context, config : Config, locale : Hash(String, JSON::Any) | Nil, status_code : Int32, message : String) - error_json_helper(env, config, locale, status_code, message, nil) +def error_json_helper(env : HTTP::Server::Context, locale : Hash(String, JSON::Any) | Nil, status_code : Int32, message : String) + error_json_helper(env, locale, status_code, message, nil) end diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index f068b5f2..7d94a6e5 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -280,9 +280,9 @@ def arg_array(array, start = 1) return args end -def make_host_url(config, kemal_config) - ssl = config.https_only || kemal_config.ssl - port = config.external_port || kemal_config.port +def make_host_url(kemal_config) + ssl = CONFIG.https_only || kemal_config.ssl + port = CONFIG.external_port || kemal_config.port if ssl scheme = "https://" @@ -297,11 +297,11 @@ def make_host_url(config, kemal_config) port = "" end - if !config.domain + if !CONFIG.domain return "" end - host = config.domain.not_nil!.lchop(".") + host = CONFIG.domain.not_nil!.lchop(".") return "#{scheme}#{host}#{port}" end @@ -345,7 +345,7 @@ def sha256(text) return digest.final.hexstring end -def subscribe_pubsub(topic, key, config) +def subscribe_pubsub(topic, key) case topic when .match(/^UC[A-Za-z0-9_-]{22}$/) topic = "channel_id=#{topic}" diff --git a/src/invidious/jobs/bypass_captcha_job.cr b/src/invidious/jobs/bypass_captcha_job.cr index 22c54036..8b1aed5f 100644 --- a/src/invidious/jobs/bypass_captcha_job.cr +++ b/src/invidious/jobs/bypass_captcha_job.cr @@ -1,9 +1,4 @@ class Invidious::Jobs::BypassCaptchaJob < Invidious::Jobs::BaseJob - private getter config : Config - - def initialize(@config) - end - def begin loop do begin @@ -22,9 +17,9 @@ class Invidious::Jobs::BypassCaptchaJob < Invidious::Jobs::BaseJob headers = response.cookies.add_request_headers(HTTP::Headers.new) - response = JSON.parse(HTTP::Client.post(config.captcha_api_url + "/createTask", + response = JSON.parse(HTTP::Client.post(CONFIG.captcha_api_url + "/createTask", headers: HTTP::Headers{"Content-Type" => "application/json"}, body: { - "clientKey" => config.captcha_key, + "clientKey" => CONFIG.captcha_key, "task" => { "type" => "NoCaptchaTaskProxyless", "websiteURL" => "https://www.youtube.com#{path}", @@ -39,9 +34,9 @@ class Invidious::Jobs::BypassCaptchaJob < Invidious::Jobs::BaseJob loop do sleep 10.seconds - response = JSON.parse(HTTP::Client.post(config.captcha_api_url + "/getTaskResult", + response = JSON.parse(HTTP::Client.post(CONFIG.captcha_api_url + "/getTaskResult", headers: HTTP::Headers{"Content-Type" => "application/json"}, body: { - "clientKey" => config.captcha_key, + "clientKey" => CONFIG.captcha_key, "taskId" => task_id, }.to_json).body) @@ -58,10 +53,10 @@ class Invidious::Jobs::BypassCaptchaJob < Invidious::Jobs::BaseJob response.cookies .select { |cookie| cookie.name != "PREF" } - .each { |cookie| config.cookies << cookie } + .each { |cookie| CONFIG.cookies << cookie } # Persist cookies between runs - File.write("config/config.yml", config.to_yaml) + File.write("config/config.yml", CONFIG.to_yaml) elsif response.headers["Location"]?.try &.includes?("/sorry/index") location = response.headers["Location"].try { |u| URI.parse(u) } headers = HTTP::Headers{":authority" => location.host.not_nil!} @@ -77,11 +72,11 @@ class Invidious::Jobs::BypassCaptchaJob < Invidious::Jobs::BaseJob inputs[node["name"]] = node["value"] end - captcha_client = HTTPClient.new(URI.parse(config.captcha_api_url)) - captcha_client.family = config.force_resolve || Socket::Family::INET + captcha_client = HTTPClient.new(URI.parse(CONFIG.captcha_api_url)) + captcha_client.family = CONFIG.force_resolve || Socket::Family::INET response = JSON.parse(captcha_client.post("/createTask", headers: HTTP::Headers{"Content-Type" => "application/json"}, body: { - "clientKey" => config.captcha_key, + "clientKey" => CONFIG.captcha_key, "task" => { "type" => "NoCaptchaTaskProxyless", "websiteURL" => location.to_s, @@ -100,7 +95,7 @@ class Invidious::Jobs::BypassCaptchaJob < Invidious::Jobs::BaseJob response = JSON.parse(captcha_client.post("/getTaskResult", headers: HTTP::Headers{"Content-Type" => "application/json"}, body: { - "clientKey" => config.captcha_key, + "clientKey" => CONFIG.captcha_key, "taskId" => task_id, }.to_json).body) @@ -119,10 +114,10 @@ class Invidious::Jobs::BypassCaptchaJob < Invidious::Jobs::BaseJob } cookies = HTTP::Cookies.from_headers(headers) - cookies.each { |cookie| config.cookies << cookie } + cookies.each { |cookie| CONFIG.cookies << cookie } # Persist cookies between runs - File.write("config/config.yml", config.to_yaml) + File.write("config/config.yml", CONFIG.to_yaml) end end rescue ex diff --git a/src/invidious/jobs/refresh_channels_job.cr b/src/invidious/jobs/refresh_channels_job.cr index 3e94a56e..fbe6d381 100644 --- a/src/invidious/jobs/refresh_channels_job.cr +++ b/src/invidious/jobs/refresh_channels_job.cr @@ -1,12 +1,11 @@ class Invidious::Jobs::RefreshChannelsJob < Invidious::Jobs::BaseJob private getter db : DB::Database - private getter config : Config - def initialize(@db, @config) + def initialize(@db) end def begin - max_fibers = config.channel_threads + max_fibers = CONFIG.channel_threads lim_fibers = max_fibers active_fibers = 0 active_channel = Channel(Bool).new @@ -31,7 +30,7 @@ class Invidious::Jobs::RefreshChannelsJob < Invidious::Jobs::BaseJob spawn do begin LOGGER.trace("RefreshChannelsJob: #{id} fiber : Fetching channel") - channel = fetch_channel(id, db, config.full_refresh) + channel = fetch_channel(id, db, CONFIG.full_refresh) lim_fibers = max_fibers diff --git a/src/invidious/jobs/refresh_feeds_job.cr b/src/invidious/jobs/refresh_feeds_job.cr index 7b4ccdea..926c27fa 100644 --- a/src/invidious/jobs/refresh_feeds_job.cr +++ b/src/invidious/jobs/refresh_feeds_job.cr @@ -1,12 +1,11 @@ class Invidious::Jobs::RefreshFeedsJob < Invidious::Jobs::BaseJob private getter db : DB::Database - private getter config : Config - def initialize(@db, @config) + def initialize(@db) end def begin - max_fibers = config.feed_threads + max_fibers = CONFIG.feed_threads active_fibers = 0 active_channel = Channel(Bool).new diff --git a/src/invidious/jobs/statistics_refresh_job.cr b/src/invidious/jobs/statistics_refresh_job.cr index 021671be..aa46fb0e 100644 --- a/src/invidious/jobs/statistics_refresh_job.cr +++ b/src/invidious/jobs/statistics_refresh_job.cr @@ -21,9 +21,8 @@ class Invidious::Jobs::StatisticsRefreshJob < Invidious::Jobs::BaseJob } private getter db : DB::Database - private getter config : Config - def initialize(@db, @config, @software_config : Hash(String, String)) + def initialize(@db, @software_config : Hash(String, String)) end def begin @@ -43,7 +42,7 @@ class Invidious::Jobs::StatisticsRefreshJob < Invidious::Jobs::BaseJob "version" => @software_config["version"], "branch" => @software_config["branch"], } - STATISTICS["openRegistration"] = config.registration_enabled + STATISTICS["openRegistration"] = CONFIG.registration_enabled end private def refresh_stats diff --git a/src/invidious/jobs/subscribe_to_feeds_job.cr b/src/invidious/jobs/subscribe_to_feeds_job.cr index 750aceb8..a431a48a 100644 --- a/src/invidious/jobs/subscribe_to_feeds_job.cr +++ b/src/invidious/jobs/subscribe_to_feeds_job.cr @@ -1,15 +1,14 @@ class Invidious::Jobs::SubscribeToFeedsJob < Invidious::Jobs::BaseJob private getter db : DB::Database private getter hmac_key : String - private getter config : Config - def initialize(@db, @config, @hmac_key) + def initialize(@db, @hmac_key) end def begin max_fibers = 1 - if config.use_pubsub_feeds.is_a?(Int32) - max_fibers = config.use_pubsub_feeds.as(Int32) + if CONFIG.use_pubsub_feeds.is_a?(Int32) + max_fibers = CONFIG.use_pubsub_feeds.as(Int32) end active_fibers = 0 @@ -30,7 +29,7 @@ class Invidious::Jobs::SubscribeToFeedsJob < Invidious::Jobs::BaseJob spawn do begin - response = subscribe_pubsub(ucid, hmac_key, config) + response = subscribe_pubsub(ucid, hmac_key) if response.status_code >= 400 LOGGER.error("SubscribeToFeedsJob: #{ucid} : #{response.body}") diff --git a/src/invidious/routes/base_route.cr b/src/invidious/routes/base_route.cr index 37624267..07c6f15b 100644 --- a/src/invidious/routes/base_route.cr +++ b/src/invidious/routes/base_route.cr @@ -1,6 +1,2 @@ abstract class Invidious::Routes::BaseRoute - private getter config : Config - - def initialize(@config) - end end diff --git a/src/invidious/routes/login.cr b/src/invidious/routes/login.cr index 45a6d4d8..662fdf13 100644 --- a/src/invidious/routes/login.cr +++ b/src/invidious/routes/login.cr @@ -6,7 +6,7 @@ class Invidious::Routes::Login < Invidious::Routes::BaseRoute return env.redirect "/feed/subscriptions" if user - if !config.login_enabled + if !CONFIG.login_enabled return error_template(400, "Login has been disabled by administrator.") end @@ -33,7 +33,7 @@ class Invidious::Routes::Login < Invidious::Routes::BaseRoute referer = get_referer(env, "/feed/subscriptions") - if !config.login_enabled + if !CONFIG.login_enabled return error_template(403, "Login has been disabled by administrator.") end @@ -274,14 +274,14 @@ class Invidious::Routes::Login < Invidious::Routes::BaseRoute 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 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 @@ -330,14 +330,14 @@ class Invidious::Routes::Login < Invidious::Routes::BaseRoute sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32)) PG_DB.exec("INSERT INTO session_ids VALUES ($1, $2, $3)", sid, email, Time.utc) - 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.utc + 2.years, + if CONFIG.domain + env.response.cookies["SID"] = HTTP::Cookie.new(name: "SID", domain: "#{CONFIG.domain}", value: sid, expires: Time.utc + 2.years, secure: secure, http_only: true) else env.response.cookies["SID"] = HTTP::Cookie.new(name: "SID", value: sid, expires: Time.utc + 2.years, @@ -354,7 +354,7 @@ class Invidious::Routes::Login < Invidious::Routes::BaseRoute env.response.cookies << cookie end else - if !config.registration_enabled + if !CONFIG.registration_enabled return error_template(400, "Registration has been disabled by administrator.") end @@ -369,7 +369,7 @@ class Invidious::Routes::Login < Invidious::Routes::BaseRoute password = password.byte_slice(0, 55) - if config.captcha_enabled + if CONFIG.captcha_enabled captcha_type = env.params.body["captcha_type"]? answer = env.params.body["answer"]? change_type = env.params.body["change_type"]? @@ -445,14 +445,14 @@ class Invidious::Routes::Login < Invidious::Routes::BaseRoute view_name = "subscriptions_#{sha256(user.email)}" PG_DB.exec("CREATE MATERIALIZED VIEW #{view_name} AS #{MATERIALIZED_VIEW_SQL.call(user.email)}") - 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.utc + 2.years, + if CONFIG.domain + env.response.cookies["SID"] = HTTP::Cookie.new(name: "SID", domain: "#{CONFIG.domain}", value: sid, expires: Time.utc + 2.years, secure: secure, http_only: true) else env.response.cookies["SID"] = HTTP::Cookie.new(name: "SID", value: sid, expires: Time.utc + 2.years, diff --git a/src/invidious/routes/user_preferences.cr b/src/invidious/routes/user_preferences.cr index 7f334115..a689a2a2 100644 --- a/src/invidious/routes/user_preferences.cr +++ b/src/invidious/routes/user_preferences.cr @@ -146,8 +146,8 @@ class Invidious::Routes::UserPreferences < Invidious::Routes::BaseRoute 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_user_preferences.default_home = env.params.body["admin_default_home"]?.try &.as(String) || config.default_user_preferences.default_home + 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 admin_feed_menu = [] of String 4.times do |index| @@ -156,40 +156,39 @@ class Invidious::Routes::UserPreferences < Invidious::Routes::BaseRoute admin_feed_menu << option end end - config.default_user_preferences.feed_menu = admin_feed_menu + CONFIG.default_user_preferences.feed_menu = admin_feed_menu popular_enabled = env.params.body["popular_enabled"]?.try &.as(String) popular_enabled ||= "off" - config.popular_enabled = popular_enabled == "on" + CONFIG.popular_enabled = popular_enabled == "on" captcha_enabled = env.params.body["captcha_enabled"]?.try &.as(String) captcha_enabled ||= "off" - config.captcha_enabled = captcha_enabled == "on" + 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" + 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" + CONFIG.registration_enabled = registration_enabled == "on" statistics_enabled = env.params.body["statistics_enabled"]?.try &.as(String) statistics_enabled ||= "off" - config.statistics_enabled = statistics_enabled == "on" + CONFIG.statistics_enabled = statistics_enabled == "on" - CONFIG.default_user_preferences = config.default_user_preferences - File.write("config/config.yml", config.to_yaml) + File.write("config/config.yml", CONFIG.to_yaml) end else - 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["PREFS"] = HTTP::Cookie.new(name: "PREFS", domain: "#{config.domain}", value: preferences, expires: Time.utc + 2.years, + if CONFIG.domain + env.response.cookies["PREFS"] = HTTP::Cookie.new(name: "PREFS", domain: "#{CONFIG.domain}", value: preferences, expires: Time.utc + 2.years, secure: secure, http_only: true) else env.response.cookies["PREFS"] = HTTP::Cookie.new(name: "PREFS", value: preferences, expires: Time.utc + 2.years, @@ -234,14 +233,14 @@ class Invidious::Routes::UserPreferences < Invidious::Routes::BaseRoute preferences = preferences.to_json - 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["PREFS"] = HTTP::Cookie.new(name: "PREFS", domain: "#{config.domain}", value: preferences, expires: Time.utc + 2.years, + if CONFIG.domain + env.response.cookies["PREFS"] = HTTP::Cookie.new(name: "PREFS", domain: "#{CONFIG.domain}", value: preferences, expires: Time.utc + 2.years, secure: secure, http_only: true) else env.response.cookies["PREFS"] = HTTP::Cookie.new(name: "PREFS", value: preferences, expires: Time.utc + 2.years, diff --git a/src/invidious/routing.cr b/src/invidious/routing.cr index 593c7372..82d0028b 100644 --- a/src/invidious/routing.cr +++ b/src/invidious/routing.cr @@ -1,14 +1,14 @@ module Invidious::Routing macro get(path, controller, method = :handle) get {{ path }} do |env| - controller_instance = {{ controller }}.new(config) + controller_instance = {{ controller }}.new controller_instance.{{ method.id }}(env) end end macro post(path, controller, method = :handle) post {{ path }} do |env| - controller_instance = {{ controller }}.new(config) + controller_instance = {{ controller }}.new controller_instance.{{ method.id }}(env) end end diff --git a/src/invidious/views/preferences.ecr b/src/invidious/views/preferences.ecr index 1ef080be..14d63536 100644 --- a/src/invidious/views/preferences.ecr +++ b/src/invidious/views/preferences.ecr @@ -208,14 +208,14 @@
<% # Web notifications are only supported over HTTPS %> - <% if Kemal.config.ssl || config.https_only %> + <% if Kemal.config.ssl || CONFIG.https_only %> <% end %> <% end %> - <% if env.get?("user") && config.admins.includes? env.get?("user").as(User).email %> + <% if env.get?("user") && CONFIG.admins.includes? env.get?("user").as(User).email %> <%= translate(locale, "Administrator preferences") %>
@@ -240,28 +240,28 @@
- checked<% end %>> + checked<% end %>>
- checked<% end %>> + checked<% end %>>
- checked<% end %>> + checked<% end %>>
- checked<% end %>> + checked<% end %>>
- checked<% end %>> + checked<% end %>>
<% end %> diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr index f6e5262d..61b900e3 100644 --- a/src/invidious/views/template.ecr +++ b/src/invidious/views/template.ecr @@ -87,7 +87,7 @@
- <% if config.login_enabled %> + <% if CONFIG.login_enabled %>
" class="pure-menu-heading"> <%= translate(locale, "Log in") %> From 70e14f92a46dd15bf118d41fd6ab5bcd2c3ee807 Mon Sep 17 00:00:00 2001 From: saltycrys <73420320+saltycrys@users.noreply.github.com> Date: Sat, 23 Jan 2021 19:41:50 +0100 Subject: [PATCH 0302/2930] Only start refresh jobs when necessary If `channel_threads` or `feed_threads` is set to zero the corresponding job is now not started. --- src/invidious.cr | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index 66a88f56..983e6196 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -162,8 +162,13 @@ end # Start jobs -Invidious::Jobs.register Invidious::Jobs::RefreshChannelsJob.new(PG_DB) -Invidious::Jobs.register Invidious::Jobs::RefreshFeedsJob.new(PG_DB) +if CONFIG.channel_threads > 0 + Invidious::Jobs.register Invidious::Jobs::RefreshChannelsJob.new(PG_DB) +end + +if CONFIG.feed_threads > 0 + Invidious::Jobs.register Invidious::Jobs::RefreshFeedsJob.new(PG_DB) +end DECRYPT_FUNCTION = DecryptFunction.new(CONFIG.decrypt_polling) if CONFIG.decrypt_polling From d0dbbd1cb1b34761046f275c4e76f8c4e6a982cb Mon Sep 17 00:00:00 2001 From: Andrew Zhao Date: Wed, 27 Jan 2021 12:36:24 -0500 Subject: [PATCH 0303/2930] remove https from channel thumbnail in search --- src/invidious/helpers/helpers.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr index 1f56ec92..6bbb18cb 100644 --- a/src/invidious/helpers/helpers.cr +++ b/src/invidious/helpers/helpers.cr @@ -225,7 +225,7 @@ def extract_item(item : JSON::Any, author_fallback : String? = nil, author_id_fa author = i["title"]["simpleText"]?.try &.as_s || author_fallback || "" author_id = i["channelId"]?.try &.as_s || author_id_fallback || "" - author_thumbnail = i["thumbnail"]["thumbnails"]?.try &.as_a[0]?.try { |u| "https:#{u["url"]}" } || "" + author_thumbnail = i["thumbnail"]["thumbnails"]?.try &.as_a[0]?.try &.["url"]?.try &.as_s || "" subscriber_count = i["subscriberCountText"]?.try &.["simpleText"]?.try &.as_s.try { |s| short_text_to_number(s.split(" ")[0]) } || 0 auto_generated = false From e35345f1358d98fa36b8d3b8b5b420260a8aef10 Mon Sep 17 00:00:00 2001 From: Perflyst Date: Thu, 28 Jan 2021 12:51:34 +0100 Subject: [PATCH 0304/2930] Remove container release on PR --- .github/workflows/container-release.yml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/.github/workflows/container-release.yml b/.github/workflows/container-release.yml index 137308d0..1f811b7c 100644 --- a/.github/workflows/container-release.yml +++ b/.github/workflows/container-release.yml @@ -4,8 +4,6 @@ on: push: branches: - "master" - pull_request: - branches: "*" schedule: - cron: 0 0 * * * @@ -39,13 +37,3 @@ jobs: labels: quay.expires-after=12w push: true tags: quay.io/invidious/invidious:${{ github.sha }},quay.io/invidious/invidious:latest - - - name: Build and push for Pull Request - if: github.ref != 'refs/heads/master' - uses: docker/build-push-action@v2 - with: - context: . - file: docker/Dockerfile - labels: quay.expires-after=6w - push: true - tags: quay.io/invidious/invidious:${{ github.sha }} From fedaef5d1704778978c27a98cf385367e149aaca Mon Sep 17 00:00:00 2001 From: Andrew Zhao Date: Fri, 29 Jan 2021 12:36:19 -0500 Subject: [PATCH 0305/2930] install crystal 35.1 in ci --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fde6506e..fc0e8096 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,8 @@ jobs: - name: Install Crystal uses: oprypin/install-crystal@v1.2.4 + with: + crystal: 0.35.1 - name: Cache Shards uses: actions/cache@v2 From 4a0b10984ad4151a8f7c8b0a9db3ed378a6df57e Mon Sep 17 00:00:00 2001 From: Andrew Zhao Date: Wed, 27 Jan 2021 10:45:03 -0500 Subject: [PATCH 0306/2930] Bump videojs and fix webworker --- assets/css/video-js.min.css | 2 +- assets/js/global.js | 3 --- assets/js/video.min.js | 25 +++++++++++-------- src/invidious.cr | 2 +- .../views/components/player_sources.ecr | 1 - 5 files changed, 17 insertions(+), 16 deletions(-) delete mode 100644 assets/js/global.js diff --git a/assets/css/video-js.min.css b/assets/css/video-js.min.css index b453b88c..17702b1b 100644 --- a/assets/css/video-js.min.css +++ b/assets/css/video-js.min.css @@ -1 +1 @@ -@charset "UTF-8";.video-js .vjs-big-play-button .vjs-icon-placeholder:before,.video-js .vjs-modal-dialog,.vjs-button>.vjs-icon-placeholder:before,.vjs-modal-dialog .vjs-modal-dialog-content{position:absolute;top:0;left:0;width:100%;height:100%}.video-js .vjs-big-play-button .vjs-icon-placeholder:before,.vjs-button>.vjs-icon-placeholder:before{text-align:center}@font-face{font-family:VideoJS;src:url(data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAABDkAAsAAAAAG6gAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABCAAAADsAAABUIIslek9TLzIAAAFEAAAAPgAAAFZRiV3hY21hcAAAAYQAAADaAAADPv749/pnbHlmAAACYAAAC3AAABHQZg6OcWhlYWQAAA3QAAAAKwAAADYZw251aGhlYQAADfwAAAAdAAAAJA+RCLFobXR4AAAOHAAAABMAAACM744AAGxvY2EAAA4wAAAASAAAAEhF6kqubWF4cAAADngAAAAfAAAAIAE0AIFuYW1lAAAOmAAAASUAAAIK1cf1oHBvc3QAAA/AAAABJAAAAdPExYuNeJxjYGRgYOBiMGCwY2BycfMJYeDLSSzJY5BiYGGAAJA8MpsxJzM9kYEDxgPKsYBpDiBmg4gCACY7BUgAeJxjYGS7wTiBgZWBgaWQ5RkDA8MvCM0cwxDOeI6BgYmBlZkBKwhIc01hcPjI+FGJHcRdyA4RZgQRADK3CxEAAHic7dFZbsMgAEXRS0ycyZnnOeG7y+qC8pU1dHusIOXxuoxaOlwZYWQB0Aea4quIEN4E9LzKbKjzDeM6H/mua6Lmc/p8yhg0lvdYx15ZG8uOLQOGjMp3EzqmzJizYMmKNRu27Nhz4MiJMxeu3Ljz4Ekqm7T8P52G8PP3lnTOVk++Z6iN6QZzNN1F7ptuN7eGOjDUoaGODHVsuvU8MdTO9Hd5aqgzQ50b6sJQl4a6MtS1oW4MdWuoO0PdG+rBUI+GejLUs6FeDPVqqDdDvRvqw1CfhpqM9At0iFLaAAB4nJ1YDXBTVRZ+5/22TUlJ8we0pHlJm7RJf5O8F2j6EymlSPkpxaL8U2xpa3DKj0CBhc2IW4eWKSokIoLsuMqssM64f+jA4HSdWXXXscBq67IOs3FXZ1ZYWVyRFdo899yXtIBQZ90k7717zz3v3HPPOfd854YCCj9cL9dL0RQFOqCbGJnrHb5EayiKIWN8iA/hWBblo6hUWm8TtCDwE80WMJus/irwyxOdxeB0MDb14VNJHnXYoLLSl6FfCUYO9nYPTA8Epg9090LprfbBbZ2hY0UlJUXHQp3/vtWkS6EBv8+rPMq5u9692f/dNxJNiqwC1xPE9TCUgCsSdQWgE3XQD25lkG4CN2xmTcOXWBOyser6RN6KnGbKSbmQ3+d0OI1m2W8QzLLkI2sykrWAgJJEtA8vGGW/2Q+CmT3n8zS9wZwu2DCvtuZKZN3xkrLh36yCZuUomQSqGpY8t/25VfHVhw8z4ebGBtfLb0ya9PCaDc+8dGTvk2dsh6z7WzvowlXKUSWo9MJ15a3KrEP2loOr2Ojhw6iW6hf2BDdEccQvZGpaAy7YovSwq8kr7HGllxpd71rkS6G0Sf11sl9OvMK1+jwPPODxjUwkOim9CU3ix1wNjXDfmJSEn618Bs6lpWwUpU+8PCqLMY650zjq8VhCIP17NEKTx3eaLL+s5Pi6yJWaWjTHLR1jYzPSV9VF/6Ojdb/1kO3Mk3uhHC0x6gc1BjlKQ+nQFxTYdaJkZ7ySVxLBbhR1dsboNXp1tCYKW2LRaEzpYcIx2BKNxaL0ZaUnSqfFoiNhHKR/GkX6PWUSAaJelQaqZL1EpoHNsajSEyPSoJ9IjhIxTdjHLmwZvhRDOiFTY/YeQnvrVZmiTQtGncECXtFTBZLOVwwMRgoXHAkXzMzPn1nAJJ8jYSbMDaqN2waGLzNhih/bZynUBMpIWSg7VYi7DRx2m8ALkIdRCJwI6ArJx2EI8kaDWeTQKeAFk9fjl/1AvwktjQ1P7NjyMGQyfd4vjipX6M/i52D7Cq80kqlcxEcGXRr/FEcgs0u5uGgB4VWuMFfpdn2Re6Hi3PqzmxWKsz6+ae2Pn9hXXw/fqM859UiGC0oKYYILJBqJrsn1Z1E5qOs9rQCiUQRREjm8yJcbHF5cUJufX1vAHlefw0XgUoboS3ETfQlTxBC4SOtuE8VPRJTBSCQSjZCpk7Gqzu+masaZ2y7Zjehho4F3g82BNDkAHpORG4+OCS+f6JTPmtRn/PH1kch6d04sp7AQb25aQ/pqUyXeQ8vrebG8OYQdXOQ+585u0sdW9rqalzRURiJ+9F4MweRFrKUjl1GUYhH1A27WOHw5cTFSFPMo9EeUIGnQTZHIaJ7AHLaOKsOODaNF9jkBjYG2QEsQ2xjMUAx2bBEbeTBWMHwskBjngq56S/yfgkBnWBa4K9sqKtq2t1UI8S9He5XuBRbawAdatrQEAi30Aks2+LM8WeCbalVZkWNylvJ+dqJnzVb+OHlSoKW8nPCP7Rd+CcZ2DdWAGqJ2CBFOphgywFFCFBNtfAbGtNPBCwxvygHeYMZMY9ZboBqwq/pVrsbgN5tkv152ODlbMfiqwGMBgxa4Exz3QhovRIUp6acqZmQzRq0ypDXS2TPLT02YIkQETnOE445oOGxOmXAqUJNNG7XgupMjPq2ua9asrj5yY/yuKteO1Kx0YNJTufrirLe1mZnat7OL6rnUdCWenpW6I8mAnbsY8KWs1PuSovCW9A/Z25PQ24a7cNOqgmTkLmBMgh4THgc4b9k2IVv1/g/F5nGljwPLfOgHAzJzh45V/4+WenTzmMtR5Z7us2Tys909UHqrPY7KbckoxRvRHhmVc3cJGE97uml0R1S0jdULVl7EvZtDFVBF35N9cEdjpgmAiOlFZ+Dtoh93+D3zzHr8RRNZQhnCNMNbcegOvpEwZoL+06cJQ07h+th3fZ/7PVbVC6ngTAV/KoLFuO6+2KFcU651gEb5ugPSIb1D+Xp8V4+k3sEIGnw5mYe4If4k1lFYr6SCzmM2EQ8iWtmwjnBI9kTwe1TlfAmXh7H02by9fW2gsjKwtv0aaURKil4OdV7rDL1MXIFNrhdxohcZXYTnq47WisrKitaObbf5+yvkLi5J6lCNZZ+B6GC38VNBZBDidSS/+mSvh6s+srgC8pyKMvDtt+de3c9fU76ZPfuM8ud4Kv0fyP/LqfepMT/3oZxSqpZaTa1DaQYLY8TFsHYbWYsPoRhRWfL5eSSQbhUGgGC3YLbVMk6PitTFNGpAsNrC6D1VNBKgBHMejaiuRWEWGgsSDBTJjqWIl8kJLlsaLJ2tXDr6xGfT85bM2Q06a46x2HTgvdnV8z5YDy/27J4zt6x2VtkzjoYpkq36kaBr4eQSg7tyiVweWubXZugtadl58ydapfbORfKsDTuZ0OBgx4cfdjCf5tbWNITnL120fdOi1RV1C3uKGzNdwYLcMvZ3BxoPyTOCD1XvXTp7U10gWCVmTV9b3r2z0SkGWovb2hp9I89O8a2smlyaO8muMU+dRmtzp60IzAoFpjLr1n388boLyf0dRvxhsHZ0qbWqDkwqvvpkj4l0fY6EIXRi5sQSrAvsVYwXRy4qJ2EVtD1AN7a0HWth9ymvL1xc3WTUKK/TAHA/bXDVtVWfOMfuGxGZv4Ln/jVr9jc3j1yMv0tndmyt9Vq88Y9gH1wtLX3KWjot5++jWHgAoZZkQ14wGQ20Fli71UmKJAy4xKMSTGbVdybW7FDDAut9XpD5AzWrYO7zQ8qffqF8+Ynd/clrHcdyxGy3a/3+mfNnzC/cBsveTjnTvXf1o6vzOlZw7WtqtdmPK/Errz/6NNtD72zmNOZfbmYdTGHfoofqI79Oc+R2n1lrnL6pOm0Up7kwxhTW12Amm7WYkXR2qYrF2AmgmbAsxZjwy1xpg/m1Je2vrp8v/nz2xpmlBg4E9hrMU341wVpTOh/OfmGvAnra8q6uctr60ZQHV3Q+WMQJykMj8ZsWn2QBOmmHMB+m5pDIpTFonYigiaKAhGEiAHF7EliVnQkjoLVIMPtJpBKHYd3A8GYH9jJzrWwmHx5Qjp7vDAX0suGRym1vtm/9W1/HyR8vczfMs6Sk8DSv855/5dlX9oQq52hT8syyp2rx5Id17IAyAM3wIjQPMOHzytEB64q6D5zT91yNbnx3V/nqnd017S9Y0605k3izoXLpsxde2n38yoOV9s1LcjwzNjbdX6asnBVaBj/6/DwKwPkpcqbDG7BnsXoSqWnUAmottYF6jMSdVyYZh3zVXCjwTiwwHH6sGuRiEHQGzuRX6whZkp123oy1BWE2mEfJ/tvIRtM4ZM5bDXiMsPMaAKOTyc5uL57rqyyc5y5JE5pm1i2S2iUX0CcaQ6lC6Zog7JqSqZmYlosl2K6pwNA84zRnQW6SaALYZQGW5lhCtU/W34N6o+bKfZ8cf3/Cl/+iTX3wBzpOY4mRkeNf3rptycGSshQWgGbYt5jFc2e0+DglIrwl6DVWQ7BuwaJ3Xk1J4VL5urnLl/Wf+gHU/hZoZdKNym6lG+I34FaNeZKcSpJIo2IeCVvpdsDGfKvzJnAwmeD37Ow65ZWwSowpgwX5T69s/rB55dP5BcpgDKFV8p7q2sn/1uc93bVzT/w6UrCqDTWvfCq/oCD/qZXNoUj8BL5Kp6GU017frfNXkAtiiyf/SOCEeLqnd8R/Ql9GlCRfctS6k5chvIBuQ1zCCjoCHL2DHNHIXxMJ3kQeO8lbsUXONeSfA5EjcG6/E+KdhN4bP04vBhdi883+BFBzQbxFbvZzQeY9LNBZc0FNfn5NwfDn6rCTnTw6R8o+gfpf5hCom33cRuiTlss3KHmZjD+BPN+5gXuA2ziS/Q73mLxUkpbKN/eqwz5uK0X9F3h2d1V4nGNgZGBgAOJd776+iue3+crAzc4AAje5Bfcg0xz9YHEOBiYQBQA8FQlFAHicY2BkYGBnAAGOPgaG//85+hkYGVCBMgBGGwNYAAAAeJxjYGBgYB8EmKOPgQEAQ04BfgAAAAAAAA4AaAB+AMwA4AECAUIBbAGYAcICGAJYArQC4AMwA7AD3gQwBJYE3AUkBWYFigYgBmYGtAbqB1gIEghYCG4IhAi2COh4nGNgZGBgUGYoZWBnAAEmIOYCQgaG/2A+AwAYCQG2AHicXZBNaoNAGIZfE5PQCKFQ2lUps2oXBfOzzAESyDKBQJdGR2NQR3QSSE/QE/QEPUUPUHqsvsrXjTMw83zPvPMNCuAWP3DQDAejdm1GjzwS7pMmwi75XngAD4/CQ/oX4TFe4Qt7uMMbOzjuDc0EmXCP/C7cJ38Iu+RP4QEe8CU8pP8WHmOPX2EPz87TPo202ey2OjlnQSXV/6arOjWFmvszMWtd6CqwOlKHq6ovycLaWMWVydXKFFZnmVFlZU46tP7R2nI5ncbi/dDkfDtFBA2DDXbYkhKc+V0Bqs5Zt9JM1HQGBRTm/EezTmZNKtpcAMs9Yu6AK9caF76zoLWIWcfMGOSkVduvSWechqZsz040Ib2PY3urxBJTzriT95lipz+TN1fmAAAAeJxtkMl2wjAMRfOAhABlKm2h80C3+ajgCKKDY6cegP59TYBzukAL+z1Zsq8ctaJTTKPrsUQLbXQQI0EXKXroY4AbDDHCGBNMcYsZ7nCPB8yxwCOe8IwXvOIN7/jAJ76wxHfUqWX+OzgumWAjJMV17i0Ndlr6irLKO+qftdT7i6y4uFSUvCknay+lFYZIZaQcmfH/xIFdYn98bqhra1aKTM/6lWMnyaYirx1rFUQZFBkb2zJUtoXeJCeg0WnLtHeSFc3OtrnozNwqi0TkSpBMDB1nSde5oJXW23hTS2/T0LilglXX7dmFVxLnq5U0vYATHFk3zX3BOisoQHNDFDeZnqKDy9hRNawN7Vh727hFzcJ5c8TILrKZfH7tIPxAFP0BpLeJPA==) format("woff");font-weight:400;font-style:normal}.video-js .vjs-big-play-button .vjs-icon-placeholder:before,.video-js .vjs-play-control .vjs-icon-placeholder,.vjs-icon-play{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-big-play-button .vjs-icon-placeholder:before,.video-js .vjs-play-control .vjs-icon-placeholder:before,.vjs-icon-play:before{content:"\f101"}.vjs-icon-play-circle{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-play-circle:before{content:"\f102"}.video-js .vjs-play-control.vjs-playing .vjs-icon-placeholder,.vjs-icon-pause{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-play-control.vjs-playing .vjs-icon-placeholder:before,.vjs-icon-pause:before{content:"\f103"}.video-js .vjs-mute-control.vjs-vol-0 .vjs-icon-placeholder,.vjs-icon-volume-mute{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-mute-control.vjs-vol-0 .vjs-icon-placeholder:before,.vjs-icon-volume-mute:before{content:"\f104"}.video-js .vjs-mute-control.vjs-vol-1 .vjs-icon-placeholder,.vjs-icon-volume-low{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-mute-control.vjs-vol-1 .vjs-icon-placeholder:before,.vjs-icon-volume-low:before{content:"\f105"}.video-js .vjs-mute-control.vjs-vol-2 .vjs-icon-placeholder,.vjs-icon-volume-mid{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-mute-control.vjs-vol-2 .vjs-icon-placeholder:before,.vjs-icon-volume-mid:before{content:"\f106"}.video-js .vjs-mute-control .vjs-icon-placeholder,.vjs-icon-volume-high{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-mute-control .vjs-icon-placeholder:before,.vjs-icon-volume-high:before{content:"\f107"}.video-js .vjs-fullscreen-control .vjs-icon-placeholder,.vjs-icon-fullscreen-enter{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-fullscreen-control .vjs-icon-placeholder:before,.vjs-icon-fullscreen-enter:before{content:"\f108"}.video-js.vjs-fullscreen .vjs-fullscreen-control .vjs-icon-placeholder,.vjs-icon-fullscreen-exit{font-family:VideoJS;font-weight:400;font-style:normal}.video-js.vjs-fullscreen .vjs-fullscreen-control .vjs-icon-placeholder:before,.vjs-icon-fullscreen-exit:before{content:"\f109"}.vjs-icon-square{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-square:before{content:"\f10a"}.vjs-icon-spinner{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-spinner:before{content:"\f10b"}.video-js .vjs-subs-caps-button .vjs-icon-placeholder,.video-js .vjs-subtitles-button .vjs-icon-placeholder,.video-js.video-js:lang(en-AU) .vjs-subs-caps-button .vjs-icon-placeholder,.video-js.video-js:lang(en-GB) .vjs-subs-caps-button .vjs-icon-placeholder,.video-js.video-js:lang(en-IE) .vjs-subs-caps-button .vjs-icon-placeholder,.video-js.video-js:lang(en-NZ) .vjs-subs-caps-button .vjs-icon-placeholder,.vjs-icon-subtitles{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-subs-caps-button .vjs-icon-placeholder:before,.video-js .vjs-subtitles-button .vjs-icon-placeholder:before,.video-js.video-js:lang(en-AU) .vjs-subs-caps-button .vjs-icon-placeholder:before,.video-js.video-js:lang(en-GB) .vjs-subs-caps-button .vjs-icon-placeholder:before,.video-js.video-js:lang(en-IE) .vjs-subs-caps-button .vjs-icon-placeholder:before,.video-js.video-js:lang(en-NZ) .vjs-subs-caps-button .vjs-icon-placeholder:before,.vjs-icon-subtitles:before{content:"\f10c"}.video-js .vjs-captions-button .vjs-icon-placeholder,.video-js:lang(en) .vjs-subs-caps-button .vjs-icon-placeholder,.video-js:lang(fr-CA) .vjs-subs-caps-button .vjs-icon-placeholder,.vjs-icon-captions{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-captions-button .vjs-icon-placeholder:before,.video-js:lang(en) .vjs-subs-caps-button .vjs-icon-placeholder:before,.video-js:lang(fr-CA) .vjs-subs-caps-button .vjs-icon-placeholder:before,.vjs-icon-captions:before{content:"\f10d"}.video-js .vjs-chapters-button .vjs-icon-placeholder,.vjs-icon-chapters{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-chapters-button .vjs-icon-placeholder:before,.vjs-icon-chapters:before{content:"\f10e"}.vjs-icon-share{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-share:before{content:"\f10f"}.vjs-icon-cog{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-cog:before{content:"\f110"}.video-js .vjs-play-progress,.video-js .vjs-volume-level,.vjs-icon-circle,.vjs-seek-to-live-control .vjs-icon-placeholder{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-play-progress:before,.video-js .vjs-volume-level:before,.vjs-icon-circle:before,.vjs-seek-to-live-control .vjs-icon-placeholder:before{content:"\f111"}.vjs-icon-circle-outline{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-circle-outline:before{content:"\f112"}.vjs-icon-circle-inner-circle{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-circle-inner-circle:before{content:"\f113"}.vjs-icon-hd{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-hd:before{content:"\f114"}.video-js .vjs-control.vjs-close-button .vjs-icon-placeholder,.vjs-icon-cancel{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-control.vjs-close-button .vjs-icon-placeholder:before,.vjs-icon-cancel:before{content:"\f115"}.video-js .vjs-play-control.vjs-ended .vjs-icon-placeholder,.vjs-icon-replay{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-play-control.vjs-ended .vjs-icon-placeholder:before,.vjs-icon-replay:before{content:"\f116"}.vjs-icon-facebook{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-facebook:before{content:"\f117"}.vjs-icon-gplus{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-gplus:before{content:"\f118"}.vjs-icon-linkedin{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-linkedin:before{content:"\f119"}.vjs-icon-twitter{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-twitter:before{content:"\f11a"}.vjs-icon-tumblr{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-tumblr:before{content:"\f11b"}.vjs-icon-pinterest{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-pinterest:before{content:"\f11c"}.video-js .vjs-descriptions-button .vjs-icon-placeholder,.vjs-icon-audio-description{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-descriptions-button .vjs-icon-placeholder:before,.vjs-icon-audio-description:before{content:"\f11d"}.video-js .vjs-audio-button .vjs-icon-placeholder,.vjs-icon-audio{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-audio-button .vjs-icon-placeholder:before,.vjs-icon-audio:before{content:"\f11e"}.vjs-icon-next-item{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-next-item:before{content:"\f11f"}.vjs-icon-previous-item{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-previous-item:before{content:"\f120"}.video-js .vjs-picture-in-picture-control .vjs-icon-placeholder,.vjs-icon-picture-in-picture-enter{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-picture-in-picture-control .vjs-icon-placeholder:before,.vjs-icon-picture-in-picture-enter:before{content:"\f121"}.video-js.vjs-picture-in-picture .vjs-picture-in-picture-control .vjs-icon-placeholder,.vjs-icon-picture-in-picture-exit{font-family:VideoJS;font-weight:400;font-style:normal}.video-js.vjs-picture-in-picture .vjs-picture-in-picture-control .vjs-icon-placeholder:before,.vjs-icon-picture-in-picture-exit:before{content:"\f122"}.video-js{display:block;vertical-align:top;box-sizing:border-box;color:#fff;background-color:#000;position:relative;padding:0;font-size:10px;line-height:1;font-weight:400;font-style:normal;font-family:Arial,Helvetica,sans-serif;word-break:initial}.video-js:-moz-full-screen{position:absolute}.video-js:-webkit-full-screen{width:100%!important;height:100%!important}.video-js[tabindex="-1"]{outline:0}.video-js *,.video-js :after,.video-js :before{box-sizing:inherit}.video-js ul{font-family:inherit;font-size:inherit;line-height:inherit;list-style-position:outside;margin-left:0;margin-right:0;margin-top:0;margin-bottom:0}.video-js.vjs-16-9,.video-js.vjs-4-3,.video-js.vjs-fluid{width:100%;max-width:100%;height:0}.video-js.vjs-16-9{padding-top:56.25%}.video-js.vjs-4-3{padding-top:75%}.video-js.vjs-fill{width:100%;height:100%}.video-js .vjs-tech{position:absolute;top:0;left:0;width:100%;height:100%}body.vjs-full-window{padding:0;margin:0;height:100%}.vjs-full-window .video-js.vjs-fullscreen{position:fixed;overflow:hidden;z-index:1000;left:0;top:0;bottom:0;right:0}.video-js.vjs-fullscreen{width:100%!important;height:100%!important;padding-top:0!important}.video-js.vjs-fullscreen.vjs-user-inactive{cursor:none}.vjs-hidden{display:none!important}.vjs-disabled{opacity:.5;cursor:default}.video-js .vjs-offscreen{height:1px;left:-9999px;position:absolute;top:0;width:1px}.vjs-lock-showing{display:block!important;opacity:1;visibility:visible}.vjs-no-js{padding:20px;color:#fff;background-color:#000;font-size:18px;font-family:Arial,Helvetica,sans-serif;text-align:center;width:300px;height:150px;margin:0 auto}.vjs-no-js a,.vjs-no-js a:visited{color:#66a8cc}.video-js .vjs-big-play-button{font-size:3em;line-height:1.5em;height:1.63332em;width:3em;display:block;position:absolute;top:10px;left:10px;padding:0;cursor:pointer;opacity:1;border:.06666em solid #fff;background-color:#2b333f;background-color:rgba(43,51,63,.7);border-radius:.3em;transition:all .4s}.vjs-big-play-centered .vjs-big-play-button{top:50%;left:50%;margin-top:-.81666em;margin-left:-1.5em}.video-js .vjs-big-play-button:focus,.video-js:hover .vjs-big-play-button{border-color:#fff;background-color:#73859f;background-color:rgba(115,133,159,.5);transition:all 0s}.vjs-controls-disabled .vjs-big-play-button,.vjs-error .vjs-big-play-button,.vjs-has-started .vjs-big-play-button,.vjs-using-native-controls .vjs-big-play-button{display:none}.vjs-has-started.vjs-paused.vjs-show-big-play-button-on-pause .vjs-big-play-button{display:block}.video-js button{background:0 0;border:none;color:inherit;display:inline-block;font-size:inherit;line-height:inherit;text-transform:none;text-decoration:none;transition:none;-webkit-appearance:none;-moz-appearance:none;appearance:none}.vjs-control .vjs-button{width:100%;height:100%}.video-js .vjs-control.vjs-close-button{cursor:pointer;height:3em;position:absolute;right:0;top:.5em;z-index:2}.video-js .vjs-modal-dialog{background:rgba(0,0,0,.8);background:linear-gradient(180deg,rgba(0,0,0,.8),rgba(255,255,255,0));overflow:auto}.video-js .vjs-modal-dialog>*{box-sizing:border-box}.vjs-modal-dialog .vjs-modal-dialog-content{font-size:1.2em;line-height:1.5;padding:20px 24px;z-index:1}.vjs-menu-button{cursor:pointer}.vjs-menu-button.vjs-disabled{cursor:default}.vjs-workinghover .vjs-menu-button.vjs-disabled:hover .vjs-menu{display:none}.vjs-menu .vjs-menu-content{display:block;padding:0;margin:0;font-family:Arial,Helvetica,sans-serif;overflow:auto}.vjs-menu .vjs-menu-content>*{box-sizing:border-box}.vjs-scrubbing .vjs-control.vjs-menu-button:hover .vjs-menu{display:none}.vjs-menu li{list-style:none;margin:0;padding:.2em 0;line-height:1.4em;font-size:1.2em;text-align:center;text-transform:lowercase}.js-focus-visible .vjs-menu li.vjs-menu-item:hover,.vjs-menu li.vjs-menu-item:focus,.vjs-menu li.vjs-menu-item:hover{background-color:#73859f;background-color:rgba(115,133,159,.5)}.js-focus-visible .vjs-menu li.vjs-selected:hover,.vjs-menu li.vjs-selected,.vjs-menu li.vjs-selected:focus,.vjs-menu li.vjs-selected:hover{background-color:#fff;color:#2b333f}.vjs-menu li.vjs-menu-title{text-align:center;text-transform:uppercase;font-size:1em;line-height:2em;padding:0;margin:0 0 .3em 0;font-weight:700;cursor:default}.vjs-menu-button-popup .vjs-menu{display:none;position:absolute;bottom:0;width:10em;left:-3em;height:0;margin-bottom:1.5em;border-top-color:rgba(43,51,63,.7)}.vjs-menu-button-popup .vjs-menu .vjs-menu-content{background-color:#2b333f;background-color:rgba(43,51,63,.7);position:absolute;width:100%;bottom:1.5em;max-height:15em}.vjs-layout-tiny .vjs-menu-button-popup .vjs-menu .vjs-menu-content,.vjs-layout-x-small .vjs-menu-button-popup .vjs-menu .vjs-menu-content{max-height:5em}.vjs-layout-small .vjs-menu-button-popup .vjs-menu .vjs-menu-content{max-height:10em}.vjs-layout-medium .vjs-menu-button-popup .vjs-menu .vjs-menu-content{max-height:14em}.vjs-layout-huge .vjs-menu-button-popup .vjs-menu .vjs-menu-content,.vjs-layout-large .vjs-menu-button-popup .vjs-menu .vjs-menu-content,.vjs-layout-x-large .vjs-menu-button-popup .vjs-menu .vjs-menu-content{max-height:25em}.vjs-menu-button-popup .vjs-menu.vjs-lock-showing,.vjs-workinghover .vjs-menu-button-popup.vjs-hover .vjs-menu{display:block}.video-js .vjs-menu-button-inline{transition:all .4s;overflow:hidden}.video-js .vjs-menu-button-inline:before{width:2.222222222em}.video-js .vjs-menu-button-inline.vjs-slider-active,.video-js .vjs-menu-button-inline:focus,.video-js .vjs-menu-button-inline:hover,.video-js.vjs-no-flex .vjs-menu-button-inline{width:12em}.vjs-menu-button-inline .vjs-menu{opacity:0;height:100%;width:auto;position:absolute;left:4em;top:0;padding:0;margin:0;transition:all .4s}.vjs-menu-button-inline.vjs-slider-active .vjs-menu,.vjs-menu-button-inline:focus .vjs-menu,.vjs-menu-button-inline:hover .vjs-menu{display:block;opacity:1}.vjs-no-flex .vjs-menu-button-inline .vjs-menu{display:block;opacity:1;position:relative;width:auto}.vjs-no-flex .vjs-menu-button-inline.vjs-slider-active .vjs-menu,.vjs-no-flex .vjs-menu-button-inline:focus .vjs-menu,.vjs-no-flex .vjs-menu-button-inline:hover .vjs-menu{width:auto}.vjs-menu-button-inline .vjs-menu-content{width:auto;height:100%;margin:0;overflow:hidden}.video-js .vjs-control-bar{display:none;width:100%;position:absolute;bottom:0;left:0;right:0;height:3em;background-color:#2b333f;background-color:rgba(43,51,63,.7)}.vjs-has-started .vjs-control-bar{display:flex;visibility:visible;opacity:1;transition:visibility .1s,opacity .1s}.vjs-has-started.vjs-user-inactive.vjs-playing .vjs-control-bar{visibility:visible;opacity:0;transition:visibility 1s,opacity 1s}.vjs-controls-disabled .vjs-control-bar,.vjs-error .vjs-control-bar,.vjs-using-native-controls .vjs-control-bar{display:none!important}.vjs-audio.vjs-has-started.vjs-user-inactive.vjs-playing .vjs-control-bar{opacity:1;visibility:visible}.vjs-has-started.vjs-no-flex .vjs-control-bar{display:table}.video-js .vjs-control{position:relative;text-align:center;margin:0;padding:0;height:100%;width:4em;flex:none}.vjs-button>.vjs-icon-placeholder:before{font-size:1.8em;line-height:1.67}.video-js .vjs-control:focus,.video-js .vjs-control:focus:before,.video-js .vjs-control:hover:before{text-shadow:0 0 1em #fff}.video-js .vjs-control-text{border:0;clip:rect(0 0 0 0);height:1px;overflow:hidden;padding:0;position:absolute;width:1px}.vjs-no-flex .vjs-control{display:table-cell;vertical-align:middle}.video-js .vjs-custom-control-spacer{display:none}.video-js .vjs-progress-control{cursor:pointer;flex:auto;display:flex;align-items:center;min-width:4em;touch-action:none}.video-js .vjs-progress-control.disabled{cursor:default}.vjs-live .vjs-progress-control{display:none}.vjs-liveui .vjs-progress-control{display:flex;align-items:center}.vjs-no-flex .vjs-progress-control{width:auto}.video-js .vjs-progress-holder{flex:auto;transition:all .2s;height:.3em}.video-js .vjs-progress-control .vjs-progress-holder{margin:0 10px}.video-js .vjs-progress-control:hover .vjs-progress-holder{font-size:1.6666666667em}.video-js .vjs-progress-control:hover .vjs-progress-holder.disabled{font-size:1em}.video-js .vjs-progress-holder .vjs-load-progress,.video-js .vjs-progress-holder .vjs-load-progress div,.video-js .vjs-progress-holder .vjs-play-progress{position:absolute;display:block;height:100%;margin:0;padding:0;width:0}.video-js .vjs-play-progress{background-color:#fff}.video-js .vjs-play-progress:before{font-size:.9em;position:absolute;right:-.5em;top:-.3333333333em;z-index:1}.video-js .vjs-load-progress{background:rgba(115,133,159,.5)}.video-js .vjs-load-progress div{background:rgba(115,133,159,.75)}.video-js .vjs-time-tooltip{background-color:#fff;background-color:rgba(255,255,255,.8);border-radius:.3em;color:#000;float:right;font-family:Arial,Helvetica,sans-serif;font-size:1em;padding:6px 8px 8px 8px;pointer-events:none;position:absolute;top:-3.4em;visibility:hidden;z-index:1}.video-js .vjs-progress-holder:focus .vjs-time-tooltip{display:none}.video-js .vjs-progress-control:hover .vjs-progress-holder:focus .vjs-time-tooltip,.video-js .vjs-progress-control:hover .vjs-time-tooltip{display:block;font-size:.6em;visibility:visible}.video-js .vjs-progress-control.disabled:hover .vjs-time-tooltip{font-size:1em}.video-js .vjs-progress-control .vjs-mouse-display{display:none;position:absolute;width:1px;height:100%;background-color:#000;z-index:1}.vjs-no-flex .vjs-progress-control .vjs-mouse-display{z-index:0}.video-js .vjs-progress-control:hover .vjs-mouse-display{display:block}.video-js.vjs-user-inactive .vjs-progress-control .vjs-mouse-display{visibility:hidden;opacity:0;transition:visibility 1s,opacity 1s}.video-js.vjs-user-inactive.vjs-no-flex .vjs-progress-control .vjs-mouse-display{display:none}.vjs-mouse-display .vjs-time-tooltip{color:#fff;background-color:#000;background-color:rgba(0,0,0,.8)}.video-js .vjs-slider{position:relative;cursor:pointer;padding:0;margin:0 .45em 0 .45em;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:#73859f;background-color:rgba(115,133,159,.5)}.video-js .vjs-slider.disabled{cursor:default}.video-js .vjs-slider:focus{text-shadow:0 0 1em #fff;box-shadow:0 0 1em #fff}.video-js .vjs-mute-control{cursor:pointer;flex:none}.video-js .vjs-volume-control{cursor:pointer;margin-right:1em;display:flex}.video-js .vjs-volume-control.vjs-volume-horizontal{width:5em}.video-js .vjs-volume-panel .vjs-volume-control{visibility:visible;opacity:0;width:1px;height:1px;margin-left:-1px}.video-js .vjs-volume-panel{transition:width 1s}.video-js .vjs-volume-panel .vjs-volume-control.vjs-slider-active,.video-js .vjs-volume-panel .vjs-volume-control:active,.video-js .vjs-volume-panel.vjs-hover .vjs-mute-control~.vjs-volume-control,.video-js .vjs-volume-panel.vjs-hover .vjs-volume-control,.video-js .vjs-volume-panel:active .vjs-volume-control,.video-js .vjs-volume-panel:focus .vjs-volume-control{visibility:visible;opacity:1;position:relative;transition:visibility .1s,opacity .1s,height .1s,width .1s,left 0s,top 0s}.video-js .vjs-volume-panel .vjs-volume-control.vjs-slider-active.vjs-volume-horizontal,.video-js .vjs-volume-panel .vjs-volume-control:active.vjs-volume-horizontal,.video-js .vjs-volume-panel.vjs-hover .vjs-mute-control~.vjs-volume-control.vjs-volume-horizontal,.video-js .vjs-volume-panel.vjs-hover .vjs-volume-control.vjs-volume-horizontal,.video-js .vjs-volume-panel:active .vjs-volume-control.vjs-volume-horizontal,.video-js .vjs-volume-panel:focus .vjs-volume-control.vjs-volume-horizontal{width:5em;height:3em;margin-right:0}.video-js .vjs-volume-panel .vjs-volume-control.vjs-slider-active.vjs-volume-vertical,.video-js .vjs-volume-panel .vjs-volume-control:active.vjs-volume-vertical,.video-js .vjs-volume-panel.vjs-hover .vjs-mute-control~.vjs-volume-control.vjs-volume-vertical,.video-js .vjs-volume-panel.vjs-hover .vjs-volume-control.vjs-volume-vertical,.video-js .vjs-volume-panel:active .vjs-volume-control.vjs-volume-vertical,.video-js .vjs-volume-panel:focus .vjs-volume-control.vjs-volume-vertical{left:-3.5em;transition:left 0s}.video-js .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-hover,.video-js .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-slider-active,.video-js .vjs-volume-panel.vjs-volume-panel-horizontal:active{width:10em;transition:width .1s}.video-js .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-mute-toggle-only{width:4em}.video-js .vjs-volume-panel .vjs-volume-control.vjs-volume-vertical{height:8em;width:3em;left:-3000em;transition:visibility 1s,opacity 1s,height 1s 1s,width 1s 1s,left 1s 1s,top 1s 1s}.video-js .vjs-volume-panel .vjs-volume-control.vjs-volume-horizontal{transition:visibility 1s,opacity 1s,height 1s 1s,width 1s,left 1s 1s,top 1s 1s}.video-js.vjs-no-flex .vjs-volume-panel .vjs-volume-control.vjs-volume-horizontal{width:5em;height:3em;visibility:visible;opacity:1;position:relative;transition:none}.video-js.vjs-no-flex .vjs-volume-control.vjs-volume-vertical,.video-js.vjs-no-flex .vjs-volume-panel .vjs-volume-control.vjs-volume-vertical{position:absolute;bottom:3em;left:.5em}.video-js .vjs-volume-panel{display:flex}.video-js .vjs-volume-bar{margin:1.35em .45em}.vjs-volume-bar.vjs-slider-horizontal{width:5em;height:.3em}.vjs-volume-bar.vjs-slider-vertical{width:.3em;height:5em;margin:1.35em auto}.video-js .vjs-volume-level{position:absolute;bottom:0;left:0;background-color:#fff}.video-js .vjs-volume-level:before{position:absolute;font-size:.9em}.vjs-slider-vertical .vjs-volume-level{width:.3em}.vjs-slider-vertical .vjs-volume-level:before{top:-.5em;left:-.3em}.vjs-slider-horizontal .vjs-volume-level{height:.3em}.vjs-slider-horizontal .vjs-volume-level:before{top:-.3em;right:-.5em}.video-js .vjs-volume-panel.vjs-volume-panel-vertical{width:4em}.vjs-volume-bar.vjs-slider-vertical .vjs-volume-level{height:100%}.vjs-volume-bar.vjs-slider-horizontal .vjs-volume-level{width:100%}.video-js .vjs-volume-vertical{width:3em;height:8em;bottom:8em;background-color:#2b333f;background-color:rgba(43,51,63,.7)}.video-js .vjs-volume-horizontal .vjs-menu{left:-2em}.vjs-poster{display:inline-block;vertical-align:middle;background-repeat:no-repeat;background-position:50% 50%;background-size:contain;background-color:#000;cursor:pointer;margin:0;padding:0;position:absolute;top:0;right:0;bottom:0;left:0;height:100%}.vjs-has-started .vjs-poster{display:none}.vjs-audio.vjs-has-started .vjs-poster{display:block}.vjs-using-native-controls .vjs-poster{display:none}.video-js .vjs-live-control{display:flex;align-items:flex-start;flex:auto;font-size:1em;line-height:3em}.vjs-no-flex .vjs-live-control{display:table-cell;width:auto;text-align:left}.video-js.vjs-liveui .vjs-live-control,.video-js:not(.vjs-live) .vjs-live-control{display:none}.video-js .vjs-seek-to-live-control{cursor:pointer;flex:none;display:inline-flex;height:100%;padding-left:.5em;padding-right:.5em;font-size:1em;line-height:3em;width:auto;min-width:4em}.vjs-no-flex .vjs-seek-to-live-control{display:table-cell;width:auto;text-align:left}.video-js.vjs-live:not(.vjs-liveui) .vjs-seek-to-live-control,.video-js:not(.vjs-live) .vjs-seek-to-live-control{display:none}.vjs-seek-to-live-control.vjs-control.vjs-at-live-edge{cursor:auto}.vjs-seek-to-live-control .vjs-icon-placeholder{margin-right:.5em;color:#888}.vjs-seek-to-live-control.vjs-control.vjs-at-live-edge .vjs-icon-placeholder{color:red}.video-js .vjs-time-control{flex:none;font-size:1em;line-height:3em;min-width:2em;width:auto;padding-left:1em;padding-right:1em}.vjs-live .vjs-time-control{display:none}.video-js .vjs-current-time,.vjs-no-flex .vjs-current-time{display:none}.video-js .vjs-duration,.vjs-no-flex .vjs-duration{display:none}.vjs-time-divider{display:none;line-height:3em}.vjs-live .vjs-time-divider{display:none}.video-js .vjs-play-control{cursor:pointer}.video-js .vjs-play-control .vjs-icon-placeholder{flex:none}.vjs-text-track-display{position:absolute;bottom:3em;left:0;right:0;top:0;pointer-events:none}.video-js.vjs-user-inactive.vjs-playing .vjs-text-track-display{bottom:1em}.video-js .vjs-text-track{font-size:1.4em;text-align:center;margin-bottom:.1em}.vjs-subtitles{color:#fff}.vjs-captions{color:#fc6}.vjs-tt-cue{display:block}video::-webkit-media-text-track-display{transform:translateY(-3em)}.video-js.vjs-user-inactive.vjs-playing video::-webkit-media-text-track-display{transform:translateY(-1.5em)}.video-js .vjs-picture-in-picture-control{cursor:pointer;flex:none}.video-js .vjs-fullscreen-control{cursor:pointer;flex:none}.vjs-playback-rate .vjs-playback-rate-value,.vjs-playback-rate>.vjs-menu-button{position:absolute;top:0;left:0;width:100%;height:100%}.vjs-playback-rate .vjs-playback-rate-value{pointer-events:none;font-size:1.5em;line-height:2;text-align:center}.vjs-playback-rate .vjs-menu{width:4em;left:0}.vjs-error .vjs-error-display .vjs-modal-dialog-content{font-size:1.4em;text-align:center}.vjs-error .vjs-error-display:before{color:#fff;content:"X";font-family:Arial,Helvetica,sans-serif;font-size:4em;left:0;line-height:1;margin-top:-.5em;position:absolute;text-shadow:.05em .05em .1em #000;text-align:center;top:50%;vertical-align:middle;width:100%}.vjs-loading-spinner{display:none;position:absolute;top:50%;left:50%;margin:-25px 0 0 -25px;opacity:.85;text-align:left;border:6px solid rgba(43,51,63,.7);box-sizing:border-box;background-clip:padding-box;width:50px;height:50px;border-radius:25px;visibility:hidden}.vjs-seeking .vjs-loading-spinner,.vjs-waiting .vjs-loading-spinner{display:block;-webkit-animation:vjs-spinner-show 0s linear .3s forwards;animation:vjs-spinner-show 0s linear .3s forwards}.vjs-loading-spinner:after,.vjs-loading-spinner:before{content:"";position:absolute;margin:-6px;box-sizing:inherit;width:inherit;height:inherit;border-radius:inherit;opacity:1;border:inherit;border-color:transparent;border-top-color:#fff}.vjs-seeking .vjs-loading-spinner:after,.vjs-seeking .vjs-loading-spinner:before,.vjs-waiting .vjs-loading-spinner:after,.vjs-waiting .vjs-loading-spinner:before{-webkit-animation:vjs-spinner-spin 1.1s cubic-bezier(.6,.2,0,.8) infinite,vjs-spinner-fade 1.1s linear infinite;animation:vjs-spinner-spin 1.1s cubic-bezier(.6,.2,0,.8) infinite,vjs-spinner-fade 1.1s linear infinite}.vjs-seeking .vjs-loading-spinner:before,.vjs-waiting .vjs-loading-spinner:before{border-top-color:#fff}.vjs-seeking .vjs-loading-spinner:after,.vjs-waiting .vjs-loading-spinner:after{border-top-color:#fff;-webkit-animation-delay:.44s;animation-delay:.44s}@keyframes vjs-spinner-show{to{visibility:visible}}@-webkit-keyframes vjs-spinner-show{to{visibility:visible}}@keyframes vjs-spinner-spin{100%{transform:rotate(360deg)}}@-webkit-keyframes vjs-spinner-spin{100%{-webkit-transform:rotate(360deg)}}@keyframes vjs-spinner-fade{0%{border-top-color:#73859f}20%{border-top-color:#73859f}35%{border-top-color:#fff}60%{border-top-color:#73859f}100%{border-top-color:#73859f}}@-webkit-keyframes vjs-spinner-fade{0%{border-top-color:#73859f}20%{border-top-color:#73859f}35%{border-top-color:#fff}60%{border-top-color:#73859f}100%{border-top-color:#73859f}}.vjs-chapters-button .vjs-menu ul{width:24em}.video-js .vjs-subs-caps-button+.vjs-menu .vjs-captions-menu-item .vjs-menu-item-text .vjs-icon-placeholder{vertical-align:middle;display:inline-block;margin-bottom:-.1em}.video-js .vjs-subs-caps-button+.vjs-menu .vjs-captions-menu-item .vjs-menu-item-text .vjs-icon-placeholder:before{font-family:VideoJS;content:"";font-size:1.5em;line-height:inherit}.video-js .vjs-audio-button+.vjs-menu .vjs-main-desc-menu-item .vjs-menu-item-text .vjs-icon-placeholder{vertical-align:middle;display:inline-block;margin-bottom:-.1em}.video-js .vjs-audio-button+.vjs-menu .vjs-main-desc-menu-item .vjs-menu-item-text .vjs-icon-placeholder:before{font-family:VideoJS;content:" ";font-size:1.5em;line-height:inherit}.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-audio-button,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-captions-button,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-chapters-button,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-current-time,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-descriptions-button,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-duration,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-playback-rate,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-remaining-time,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-subtitles-button,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-time-divider,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-volume-control,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-audio-button,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-captions-button,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-chapters-button,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-current-time,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-descriptions-button,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-duration,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-playback-rate,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-remaining-time,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-subtitles-button,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-time-divider,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-volume-control,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-audio-button,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-captions-button,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-chapters-button,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-current-time,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-descriptions-button,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-duration,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-playback-rate,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-remaining-time,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-subtitles-button,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-time-divider,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-volume-control{display:none}.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-slider-active,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-volume-panel.vjs-volume-panel-horizontal:active,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-volume-panel.vjs-volume-panel-horizontal:hover,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-slider-active,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-volume-panel.vjs-volume-panel-horizontal:active,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-volume-panel.vjs-volume-panel-horizontal:hover,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-slider-active,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-volume-panel.vjs-volume-panel-horizontal:active,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-volume-panel.vjs-volume-panel-horizontal:hover{width:auto;width:initial}.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-subs-caps-button,.video-js:not(.vjs-fullscreen).vjs-layout-x-small:not(.vjs-live) .vjs-subs-caps-button,.video-js:not(.vjs-fullscreen).vjs-layout-x-small:not(.vjs-liveui) .vjs-subs-caps-button{display:none}.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-custom-control-spacer,.video-js:not(.vjs-fullscreen).vjs-layout-x-small.vjs-liveui .vjs-custom-control-spacer{flex:auto;display:block}.video-js:not(.vjs-fullscreen).vjs-layout-tiny.vjs-no-flex .vjs-custom-control-spacer,.video-js:not(.vjs-fullscreen).vjs-layout-x-small.vjs-liveui.vjs-no-flex .vjs-custom-control-spacer{width:auto}.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-progress-control,.video-js:not(.vjs-fullscreen).vjs-layout-x-small.vjs-liveui .vjs-progress-control{display:none}.vjs-modal-dialog.vjs-text-track-settings{background-color:#2b333f;background-color:rgba(43,51,63,.75);color:#fff;height:70%}.vjs-text-track-settings .vjs-modal-dialog-content{display:table}.vjs-text-track-settings .vjs-track-settings-colors,.vjs-text-track-settings .vjs-track-settings-controls,.vjs-text-track-settings .vjs-track-settings-font{display:table-cell}.vjs-text-track-settings .vjs-track-settings-controls{text-align:right;vertical-align:bottom}@supports (display:grid){.vjs-text-track-settings .vjs-modal-dialog-content{display:grid;grid-template-columns:1fr 1fr;grid-template-rows:1fr;padding:20px 24px 0 24px}.vjs-track-settings-controls .vjs-default-button{margin-bottom:20px}.vjs-text-track-settings .vjs-track-settings-controls{grid-column:1/-1}.vjs-layout-small .vjs-text-track-settings .vjs-modal-dialog-content,.vjs-layout-tiny .vjs-text-track-settings .vjs-modal-dialog-content,.vjs-layout-x-small .vjs-text-track-settings .vjs-modal-dialog-content{grid-template-columns:1fr}}.vjs-track-setting>select{margin-right:1em;margin-bottom:.5em}.vjs-text-track-settings fieldset{margin:5px;padding:3px;border:none}.vjs-text-track-settings fieldset span{display:inline-block}.vjs-text-track-settings fieldset span>select{max-width:7.3em}.vjs-text-track-settings legend{color:#fff;margin:0 0 5px 0}.vjs-text-track-settings .vjs-label{position:absolute;clip:rect(1px 1px 1px 1px);clip:rect(1px,1px,1px,1px);display:block;margin:0 0 5px 0;padding:0;border:0;height:1px;width:1px;overflow:hidden}.vjs-track-settings-controls button:active,.vjs-track-settings-controls button:focus{outline-style:solid;outline-width:medium;background-image:linear-gradient(0deg,#fff 88%,#73859f 100%)}.vjs-track-settings-controls button:hover{color:rgba(43,51,63,.75)}.vjs-track-settings-controls button{background-color:#fff;background-image:linear-gradient(-180deg,#fff 88%,#73859f 100%);color:#2b333f;cursor:pointer;border-radius:2px}.vjs-track-settings-controls .vjs-default-button{margin-right:1em}@media print{.video-js>:not(.vjs-tech):not(.vjs-poster){visibility:hidden}}.vjs-resize-manager{position:absolute;top:0;left:0;width:100%;height:100%;border:none;z-index:-1000}.js-focus-visible .video-js :focus:not(.focus-visible){outline:0;background:0 0}.video-js .vjs-menu :focus:not(:focus-visible),.video-js :focus:not(:focus-visible){outline:0;background:0 0} \ No newline at end of file +@charset "UTF-8";.video-js .vjs-big-play-button .vjs-icon-placeholder:before,.video-js .vjs-modal-dialog,.vjs-button>.vjs-icon-placeholder:before,.vjs-modal-dialog .vjs-modal-dialog-content{position:absolute;top:0;left:0;width:100%;height:100%}.video-js .vjs-big-play-button .vjs-icon-placeholder:before,.vjs-button>.vjs-icon-placeholder:before{text-align:center}@font-face{font-family:VideoJS;src:url(data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAABDkAAsAAAAAG6gAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABCAAAADsAAABUIIslek9TLzIAAAFEAAAAPgAAAFZRiV3hY21hcAAAAYQAAADaAAADPv749/pnbHlmAAACYAAAC3AAABHQZg6OcWhlYWQAAA3QAAAAKwAAADYZw251aGhlYQAADfwAAAAdAAAAJA+RCLFobXR4AAAOHAAAABMAAACM744AAGxvY2EAAA4wAAAASAAAAEhF6kqubWF4cAAADngAAAAfAAAAIAE0AIFuYW1lAAAOmAAAASUAAAIK1cf1oHBvc3QAAA/AAAABJAAAAdPExYuNeJxjYGRgYOBiMGCwY2BycfMJYeDLSSzJY5BiYGGAAJA8MpsxJzM9kYEDxgPKsYBpDiBmg4gCACY7BUgAeJxjYGS7wTiBgZWBgaWQ5RkDA8MvCM0cwxDOeI6BgYmBlZkBKwhIc01hcPjI+FGJHcRdyA4RZgQRADK3CxEAAHic7dFZbsMgAEXRS0ycyZnnOeG7y+qC8pU1dHusIOXxuoxaOlwZYWQB0Aea4quIEN4E9LzKbKjzDeM6H/mua6Lmc/p8yhg0lvdYx15ZG8uOLQOGjMp3EzqmzJizYMmKNRu27Nhz4MiJMxeu3Ljz4Ekqm7T8P52G8PP3lnTOVk++Z6iN6QZzNN1F7ptuN7eGOjDUoaGODHVsuvU8MdTO9Hd5aqgzQ50b6sJQl4a6MtS1oW4MdWuoO0PdG+rBUI+GejLUs6FeDPVqqDdDvRvqw1CfhpqM9At0iFLaAAB4nJ1YDXBTVRZ+5/22TUlJ8we0pHlJm7RJf5O8F2j6EymlSPkpxaL8U2xpa3DKj0CBhc2IW4eWKSokIoLsuMqssM64f+jA4HSdWXXXscBq67IOs3FXZ1ZYWVyRFdo899yXtIBQZ90k7717zz3v3HPPOfd854YCCj9cL9dL0RQFOqCbGJnrHb5EayiKIWN8iA/hWBblo6hUWm8TtCDwE80WMJus/irwyxOdxeB0MDb14VNJHnXYoLLSl6FfCUYO9nYPTA8Epg9090LprfbBbZ2hY0UlJUXHQp3/vtWkS6EBv8+rPMq5u9692f/dNxJNiqwC1xPE9TCUgCsSdQWgE3XQD25lkG4CN2xmTcOXWBOyser6RN6KnGbKSbmQ3+d0OI1m2W8QzLLkI2sykrWAgJJEtA8vGGW/2Q+CmT3n8zS9wZwu2DCvtuZKZN3xkrLh36yCZuUomQSqGpY8t/25VfHVhw8z4ebGBtfLb0ya9PCaDc+8dGTvk2dsh6z7WzvowlXKUSWo9MJ15a3KrEP2loOr2Ojhw6iW6hf2BDdEccQvZGpaAy7YovSwq8kr7HGllxpd71rkS6G0Sf11sl9OvMK1+jwPPODxjUwkOim9CU3ix1wNjXDfmJSEn618Bs6lpWwUpU+8PCqLMY650zjq8VhCIP17NEKTx3eaLL+s5Pi6yJWaWjTHLR1jYzPSV9VF/6Ojdb/1kO3Mk3uhHC0x6gc1BjlKQ+nQFxTYdaJkZ7ySVxLBbhR1dsboNXp1tCYKW2LRaEzpYcIx2BKNxaL0ZaUnSqfFoiNhHKR/GkX6PWUSAaJelQaqZL1EpoHNsajSEyPSoJ9IjhIxTdjHLmwZvhRDOiFTY/YeQnvrVZmiTQtGncECXtFTBZLOVwwMRgoXHAkXzMzPn1nAJJ8jYSbMDaqN2waGLzNhih/bZynUBMpIWSg7VYi7DRx2m8ALkIdRCJwI6ArJx2EI8kaDWeTQKeAFk9fjl/1AvwktjQ1P7NjyMGQyfd4vjipX6M/i52D7Cq80kqlcxEcGXRr/FEcgs0u5uGgB4VWuMFfpdn2Re6Hi3PqzmxWKsz6+ae2Pn9hXXw/fqM859UiGC0oKYYILJBqJrsn1Z1E5qOs9rQCiUQRREjm8yJcbHF5cUJufX1vAHlefw0XgUoboS3ETfQlTxBC4SOtuE8VPRJTBSCQSjZCpk7Gqzu+masaZ2y7Zjehho4F3g82BNDkAHpORG4+OCS+f6JTPmtRn/PH1kch6d04sp7AQb25aQ/pqUyXeQ8vrebG8OYQdXOQ+585u0sdW9rqalzRURiJ+9F4MweRFrKUjl1GUYhH1A27WOHw5cTFSFPMo9EeUIGnQTZHIaJ7AHLaOKsOODaNF9jkBjYG2QEsQ2xjMUAx2bBEbeTBWMHwskBjngq56S/yfgkBnWBa4K9sqKtq2t1UI8S9He5XuBRbawAdatrQEAi30Aks2+LM8WeCbalVZkWNylvJ+dqJnzVb+OHlSoKW8nPCP7Rd+CcZ2DdWAGqJ2CBFOphgywFFCFBNtfAbGtNPBCwxvygHeYMZMY9ZboBqwq/pVrsbgN5tkv152ODlbMfiqwGMBgxa4Exz3QhovRIUp6acqZmQzRq0ypDXS2TPLT02YIkQETnOE445oOGxOmXAqUJNNG7XgupMjPq2ua9asrj5yY/yuKteO1Kx0YNJTufrirLe1mZnat7OL6rnUdCWenpW6I8mAnbsY8KWs1PuSovCW9A/Z25PQ24a7cNOqgmTkLmBMgh4THgc4b9k2IVv1/g/F5nGljwPLfOgHAzJzh45V/4+WenTzmMtR5Z7us2Tys909UHqrPY7KbckoxRvRHhmVc3cJGE97uml0R1S0jdULVl7EvZtDFVBF35N9cEdjpgmAiOlFZ+Dtoh93+D3zzHr8RRNZQhnCNMNbcegOvpEwZoL+06cJQ07h+th3fZ/7PVbVC6ngTAV/KoLFuO6+2KFcU651gEb5ugPSIb1D+Xp8V4+k3sEIGnw5mYe4If4k1lFYr6SCzmM2EQ8iWtmwjnBI9kTwe1TlfAmXh7H02by9fW2gsjKwtv0aaURKil4OdV7rDL1MXIFNrhdxohcZXYTnq47WisrKitaObbf5+yvkLi5J6lCNZZ+B6GC38VNBZBDidSS/+mSvh6s+srgC8pyKMvDtt+de3c9fU76ZPfuM8ud4Kv0fyP/LqfepMT/3oZxSqpZaTa1DaQYLY8TFsHYbWYsPoRhRWfL5eSSQbhUGgGC3YLbVMk6PitTFNGpAsNrC6D1VNBKgBHMejaiuRWEWGgsSDBTJjqWIl8kJLlsaLJ2tXDr6xGfT85bM2Q06a46x2HTgvdnV8z5YDy/27J4zt6x2VtkzjoYpkq36kaBr4eQSg7tyiVweWubXZugtadl58ydapfbORfKsDTuZ0OBgx4cfdjCf5tbWNITnL120fdOi1RV1C3uKGzNdwYLcMvZ3BxoPyTOCD1XvXTp7U10gWCVmTV9b3r2z0SkGWovb2hp9I89O8a2smlyaO8muMU+dRmtzp60IzAoFpjLr1n388boLyf0dRvxhsHZ0qbWqDkwqvvpkj4l0fY6EIXRi5sQSrAvsVYwXRy4qJ2EVtD1AN7a0HWth9ymvL1xc3WTUKK/TAHA/bXDVtVWfOMfuGxGZv4Ln/jVr9jc3j1yMv0tndmyt9Vq88Y9gH1wtLX3KWjot5++jWHgAoZZkQ14wGQ20Fli71UmKJAy4xKMSTGbVdybW7FDDAut9XpD5AzWrYO7zQ8qffqF8+Ynd/clrHcdyxGy3a/3+mfNnzC/cBsveTjnTvXf1o6vzOlZw7WtqtdmPK/Errz/6NNtD72zmNOZfbmYdTGHfoofqI79Oc+R2n1lrnL6pOm0Up7kwxhTW12Amm7WYkXR2qYrF2AmgmbAsxZjwy1xpg/m1Je2vrp8v/nz2xpmlBg4E9hrMU341wVpTOh/OfmGvAnra8q6uctr60ZQHV3Q+WMQJykMj8ZsWn2QBOmmHMB+m5pDIpTFonYigiaKAhGEiAHF7EliVnQkjoLVIMPtJpBKHYd3A8GYH9jJzrWwmHx5Qjp7vDAX0suGRym1vtm/9W1/HyR8vczfMs6Sk8DSv855/5dlX9oQq52hT8syyp2rx5Id17IAyAM3wIjQPMOHzytEB64q6D5zT91yNbnx3V/nqnd017S9Y0605k3izoXLpsxde2n38yoOV9s1LcjwzNjbdX6asnBVaBj/6/DwKwPkpcqbDG7BnsXoSqWnUAmottYF6jMSdVyYZh3zVXCjwTiwwHH6sGuRiEHQGzuRX6whZkp123oy1BWE2mEfJ/tvIRtM4ZM5bDXiMsPMaAKOTyc5uL57rqyyc5y5JE5pm1i2S2iUX0CcaQ6lC6Zog7JqSqZmYlosl2K6pwNA84zRnQW6SaALYZQGW5lhCtU/W34N6o+bKfZ8cf3/Cl/+iTX3wBzpOY4mRkeNf3rptycGSshQWgGbYt5jFc2e0+DglIrwl6DVWQ7BuwaJ3Xk1J4VL5urnLl/Wf+gHU/hZoZdKNym6lG+I34FaNeZKcSpJIo2IeCVvpdsDGfKvzJnAwmeD37Ow65ZWwSowpgwX5T69s/rB55dP5BcpgDKFV8p7q2sn/1uc93bVzT/w6UrCqDTWvfCq/oCD/qZXNoUj8BL5Kp6GU017frfNXkAtiiyf/SOCEeLqnd8R/Ql9GlCRfctS6k5chvIBuQ1zCCjoCHL2DHNHIXxMJ3kQeO8lbsUXONeSfA5EjcG6/E+KdhN4bP04vBhdi883+BFBzQbxFbvZzQeY9LNBZc0FNfn5NwfDn6rCTnTw6R8o+gfpf5hCom33cRuiTlss3KHmZjD+BPN+5gXuA2ziS/Q73mLxUkpbKN/eqwz5uK0X9F3h2d1V4nGNgZGBgAOJd776+iue3+crAzc4AAje5Bfcg0xz9YHEOBiYQBQA8FQlFAHicY2BkYGBnAAGOPgaG//85+hkYGVCBMgBGGwNYAAAAeJxjYGBgYB8EmKOPgQEAQ04BfgAAAAAAAA4AaAB+AMwA4AECAUIBbAGYAcICGAJYArQC4AMwA7AD3gQwBJYE3AUkBWYFigYgBmYGtAbqB1gIEghYCG4IhAi2COh4nGNgZGBgUGYoZWBnAAEmIOYCQgaG/2A+AwAYCQG2AHicXZBNaoNAGIZfE5PQCKFQ2lUps2oXBfOzzAESyDKBQJdGR2NQR3QSSE/QE/QEPUUPUHqsvsrXjTMw83zPvPMNCuAWP3DQDAejdm1GjzwS7pMmwi75XngAD4/CQ/oX4TFe4Qt7uMMbOzjuDc0EmXCP/C7cJ38Iu+RP4QEe8CU8pP8WHmOPX2EPz87TPo202ey2OjlnQSXV/6arOjWFmvszMWtd6CqwOlKHq6ovycLaWMWVydXKFFZnmVFlZU46tP7R2nI5ncbi/dDkfDtFBA2DDXbYkhKc+V0Bqs5Zt9JM1HQGBRTm/EezTmZNKtpcAMs9Yu6AK9caF76zoLWIWcfMGOSkVduvSWechqZsz040Ib2PY3urxBJTzriT95lipz+TN1fmAAAAeJxtkMl2wjAMRfOAhABlKm2h80C3+ajgCKKDY6cegP59TYBzukAL+z1Zsq8ctaJTTKPrsUQLbXQQI0EXKXroY4AbDDHCGBNMcYsZ7nCPB8yxwCOe8IwXvOIN7/jAJ76wxHfUqWX+OzgumWAjJMV17i0Ndlr6irLKO+qftdT7i6y4uFSUvCknay+lFYZIZaQcmfH/xIFdYn98bqhra1aKTM/6lWMnyaYirx1rFUQZFBkb2zJUtoXeJCeg0WnLtHeSFc3OtrnozNwqi0TkSpBMDB1nSde5oJXW23hTS2/T0LilglXX7dmFVxLnq5U0vYATHFk3zX3BOisoQHNDFDeZnqKDy9hRNawN7Vh727hFzcJ5c8TILrKZfH7tIPxAFP0BpLeJPA==) format("woff");font-weight:400;font-style:normal}.video-js .vjs-big-play-button .vjs-icon-placeholder:before,.video-js .vjs-play-control .vjs-icon-placeholder,.vjs-icon-play{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-big-play-button .vjs-icon-placeholder:before,.video-js .vjs-play-control .vjs-icon-placeholder:before,.vjs-icon-play:before{content:"\f101"}.vjs-icon-play-circle{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-play-circle:before{content:"\f102"}.video-js .vjs-play-control.vjs-playing .vjs-icon-placeholder,.vjs-icon-pause{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-play-control.vjs-playing .vjs-icon-placeholder:before,.vjs-icon-pause:before{content:"\f103"}.video-js .vjs-mute-control.vjs-vol-0 .vjs-icon-placeholder,.vjs-icon-volume-mute{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-mute-control.vjs-vol-0 .vjs-icon-placeholder:before,.vjs-icon-volume-mute:before{content:"\f104"}.video-js .vjs-mute-control.vjs-vol-1 .vjs-icon-placeholder,.vjs-icon-volume-low{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-mute-control.vjs-vol-1 .vjs-icon-placeholder:before,.vjs-icon-volume-low:before{content:"\f105"}.video-js .vjs-mute-control.vjs-vol-2 .vjs-icon-placeholder,.vjs-icon-volume-mid{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-mute-control.vjs-vol-2 .vjs-icon-placeholder:before,.vjs-icon-volume-mid:before{content:"\f106"}.video-js .vjs-mute-control .vjs-icon-placeholder,.vjs-icon-volume-high{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-mute-control .vjs-icon-placeholder:before,.vjs-icon-volume-high:before{content:"\f107"}.video-js .vjs-fullscreen-control .vjs-icon-placeholder,.vjs-icon-fullscreen-enter{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-fullscreen-control .vjs-icon-placeholder:before,.vjs-icon-fullscreen-enter:before{content:"\f108"}.video-js.vjs-fullscreen .vjs-fullscreen-control .vjs-icon-placeholder,.vjs-icon-fullscreen-exit{font-family:VideoJS;font-weight:400;font-style:normal}.video-js.vjs-fullscreen .vjs-fullscreen-control .vjs-icon-placeholder:before,.vjs-icon-fullscreen-exit:before{content:"\f109"}.vjs-icon-square{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-square:before{content:"\f10a"}.vjs-icon-spinner{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-spinner:before{content:"\f10b"}.video-js .vjs-subs-caps-button .vjs-icon-placeholder,.video-js .vjs-subtitles-button .vjs-icon-placeholder,.video-js.video-js:lang(en-AU) .vjs-subs-caps-button .vjs-icon-placeholder,.video-js.video-js:lang(en-GB) .vjs-subs-caps-button .vjs-icon-placeholder,.video-js.video-js:lang(en-IE) .vjs-subs-caps-button .vjs-icon-placeholder,.video-js.video-js:lang(en-NZ) .vjs-subs-caps-button .vjs-icon-placeholder,.vjs-icon-subtitles{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-subs-caps-button .vjs-icon-placeholder:before,.video-js .vjs-subtitles-button .vjs-icon-placeholder:before,.video-js.video-js:lang(en-AU) .vjs-subs-caps-button .vjs-icon-placeholder:before,.video-js.video-js:lang(en-GB) .vjs-subs-caps-button .vjs-icon-placeholder:before,.video-js.video-js:lang(en-IE) .vjs-subs-caps-button .vjs-icon-placeholder:before,.video-js.video-js:lang(en-NZ) .vjs-subs-caps-button .vjs-icon-placeholder:before,.vjs-icon-subtitles:before{content:"\f10c"}.video-js .vjs-captions-button .vjs-icon-placeholder,.video-js:lang(en) .vjs-subs-caps-button .vjs-icon-placeholder,.video-js:lang(fr-CA) .vjs-subs-caps-button .vjs-icon-placeholder,.vjs-icon-captions{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-captions-button .vjs-icon-placeholder:before,.video-js:lang(en) .vjs-subs-caps-button .vjs-icon-placeholder:before,.video-js:lang(fr-CA) .vjs-subs-caps-button .vjs-icon-placeholder:before,.vjs-icon-captions:before{content:"\f10d"}.video-js .vjs-chapters-button .vjs-icon-placeholder,.vjs-icon-chapters{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-chapters-button .vjs-icon-placeholder:before,.vjs-icon-chapters:before{content:"\f10e"}.vjs-icon-share{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-share:before{content:"\f10f"}.vjs-icon-cog{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-cog:before{content:"\f110"}.video-js .vjs-play-progress,.video-js .vjs-volume-level,.vjs-icon-circle,.vjs-seek-to-live-control .vjs-icon-placeholder{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-play-progress:before,.video-js .vjs-volume-level:before,.vjs-icon-circle:before,.vjs-seek-to-live-control .vjs-icon-placeholder:before{content:"\f111"}.vjs-icon-circle-outline{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-circle-outline:before{content:"\f112"}.vjs-icon-circle-inner-circle{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-circle-inner-circle:before{content:"\f113"}.vjs-icon-hd{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-hd:before{content:"\f114"}.video-js .vjs-control.vjs-close-button .vjs-icon-placeholder,.vjs-icon-cancel{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-control.vjs-close-button .vjs-icon-placeholder:before,.vjs-icon-cancel:before{content:"\f115"}.video-js .vjs-play-control.vjs-ended .vjs-icon-placeholder,.vjs-icon-replay{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-play-control.vjs-ended .vjs-icon-placeholder:before,.vjs-icon-replay:before{content:"\f116"}.vjs-icon-facebook{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-facebook:before{content:"\f117"}.vjs-icon-gplus{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-gplus:before{content:"\f118"}.vjs-icon-linkedin{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-linkedin:before{content:"\f119"}.vjs-icon-twitter{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-twitter:before{content:"\f11a"}.vjs-icon-tumblr{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-tumblr:before{content:"\f11b"}.vjs-icon-pinterest{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-pinterest:before{content:"\f11c"}.video-js .vjs-descriptions-button .vjs-icon-placeholder,.vjs-icon-audio-description{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-descriptions-button .vjs-icon-placeholder:before,.vjs-icon-audio-description:before{content:"\f11d"}.video-js .vjs-audio-button .vjs-icon-placeholder,.vjs-icon-audio{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-audio-button .vjs-icon-placeholder:before,.vjs-icon-audio:before{content:"\f11e"}.vjs-icon-next-item{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-next-item:before{content:"\f11f"}.vjs-icon-previous-item{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-previous-item:before{content:"\f120"}.video-js .vjs-picture-in-picture-control .vjs-icon-placeholder,.vjs-icon-picture-in-picture-enter{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-picture-in-picture-control .vjs-icon-placeholder:before,.vjs-icon-picture-in-picture-enter:before{content:"\f121"}.video-js.vjs-picture-in-picture .vjs-picture-in-picture-control .vjs-icon-placeholder,.vjs-icon-picture-in-picture-exit{font-family:VideoJS;font-weight:400;font-style:normal}.video-js.vjs-picture-in-picture .vjs-picture-in-picture-control .vjs-icon-placeholder:before,.vjs-icon-picture-in-picture-exit:before{content:"\f122"}.video-js{display:block;vertical-align:top;box-sizing:border-box;color:#fff;background-color:#000;position:relative;padding:0;font-size:10px;line-height:1;font-weight:400;font-style:normal;font-family:Arial,Helvetica,sans-serif;word-break:initial}.video-js:-moz-full-screen{position:absolute}.video-js:-webkit-full-screen{width:100%!important;height:100%!important}.video-js[tabindex="-1"]{outline:0}.video-js *,.video-js :after,.video-js :before{box-sizing:inherit}.video-js ul{font-family:inherit;font-size:inherit;line-height:inherit;list-style-position:outside;margin-left:0;margin-right:0;margin-top:0;margin-bottom:0}.video-js.vjs-16-9,.video-js.vjs-4-3,.video-js.vjs-fluid{width:100%;max-width:100%;height:0}.video-js.vjs-16-9{padding-top:56.25%}.video-js.vjs-4-3{padding-top:75%}.video-js.vjs-fill{width:100%;height:100%}.video-js .vjs-tech{position:absolute;top:0;left:0;width:100%;height:100%}body.vjs-full-window{padding:0;margin:0;height:100%}.vjs-full-window .video-js.vjs-fullscreen{position:fixed;overflow:hidden;z-index:1000;left:0;top:0;bottom:0;right:0}.video-js.vjs-fullscreen:not(.vjs-ios-native-fs){width:100%!important;height:100%!important;padding-top:0!important}.video-js.vjs-fullscreen.vjs-user-inactive{cursor:none}.vjs-hidden{display:none!important}.vjs-disabled{opacity:.5;cursor:default}.video-js .vjs-offscreen{height:1px;left:-9999px;position:absolute;top:0;width:1px}.vjs-lock-showing{display:block!important;opacity:1;visibility:visible}.vjs-no-js{padding:20px;color:#fff;background-color:#000;font-size:18px;font-family:Arial,Helvetica,sans-serif;text-align:center;width:300px;height:150px;margin:0 auto}.vjs-no-js a,.vjs-no-js a:visited{color:#66a8cc}.video-js .vjs-big-play-button{font-size:3em;line-height:1.5em;height:1.63332em;width:3em;display:block;position:absolute;top:10px;left:10px;padding:0;cursor:pointer;opacity:1;border:.06666em solid #fff;background-color:#2b333f;background-color:rgba(43,51,63,.7);border-radius:.3em;transition:all .4s}.vjs-big-play-centered .vjs-big-play-button{top:50%;left:50%;margin-top:-.81666em;margin-left:-1.5em}.video-js .vjs-big-play-button:focus,.video-js:hover .vjs-big-play-button{border-color:#fff;background-color:#73859f;background-color:rgba(115,133,159,.5);transition:all 0s}.vjs-controls-disabled .vjs-big-play-button,.vjs-error .vjs-big-play-button,.vjs-has-started .vjs-big-play-button,.vjs-using-native-controls .vjs-big-play-button{display:none}.vjs-has-started.vjs-paused.vjs-show-big-play-button-on-pause .vjs-big-play-button{display:block}.video-js button{background:0 0;border:none;color:inherit;display:inline-block;font-size:inherit;line-height:inherit;text-transform:none;text-decoration:none;transition:none;-webkit-appearance:none;-moz-appearance:none;appearance:none}.vjs-control .vjs-button{width:100%;height:100%}.video-js .vjs-control.vjs-close-button{cursor:pointer;height:3em;position:absolute;right:0;top:.5em;z-index:2}.video-js .vjs-modal-dialog{background:rgba(0,0,0,.8);background:linear-gradient(180deg,rgba(0,0,0,.8),rgba(255,255,255,0));overflow:auto}.video-js .vjs-modal-dialog>*{box-sizing:border-box}.vjs-modal-dialog .vjs-modal-dialog-content{font-size:1.2em;line-height:1.5;padding:20px 24px;z-index:1}.vjs-menu-button{cursor:pointer}.vjs-menu-button.vjs-disabled{cursor:default}.vjs-workinghover .vjs-menu-button.vjs-disabled:hover .vjs-menu{display:none}.vjs-menu .vjs-menu-content{display:block;padding:0;margin:0;font-family:Arial,Helvetica,sans-serif;overflow:auto}.vjs-menu .vjs-menu-content>*{box-sizing:border-box}.vjs-scrubbing .vjs-control.vjs-menu-button:hover .vjs-menu{display:none}.vjs-menu li{list-style:none;margin:0;padding:.2em 0;line-height:1.4em;font-size:1.2em;text-align:center;text-transform:lowercase}.js-focus-visible .vjs-menu li.vjs-menu-item:hover,.vjs-menu li.vjs-menu-item:focus,.vjs-menu li.vjs-menu-item:hover{background-color:#73859f;background-color:rgba(115,133,159,.5)}.js-focus-visible .vjs-menu li.vjs-selected:hover,.vjs-menu li.vjs-selected,.vjs-menu li.vjs-selected:focus,.vjs-menu li.vjs-selected:hover{background-color:#fff;color:#2b333f}.vjs-menu li.vjs-menu-title{text-align:center;text-transform:uppercase;font-size:1em;line-height:2em;padding:0;margin:0 0 .3em 0;font-weight:700;cursor:default}.vjs-menu-button-popup .vjs-menu{display:none;position:absolute;bottom:0;width:10em;left:-3em;height:0;margin-bottom:1.5em;border-top-color:rgba(43,51,63,.7)}.vjs-menu-button-popup .vjs-menu .vjs-menu-content{background-color:#2b333f;background-color:rgba(43,51,63,.7);position:absolute;width:100%;bottom:1.5em;max-height:15em}.vjs-layout-tiny .vjs-menu-button-popup .vjs-menu .vjs-menu-content,.vjs-layout-x-small .vjs-menu-button-popup .vjs-menu .vjs-menu-content{max-height:5em}.vjs-layout-small .vjs-menu-button-popup .vjs-menu .vjs-menu-content{max-height:10em}.vjs-layout-medium .vjs-menu-button-popup .vjs-menu .vjs-menu-content{max-height:14em}.vjs-layout-huge .vjs-menu-button-popup .vjs-menu .vjs-menu-content,.vjs-layout-large .vjs-menu-button-popup .vjs-menu .vjs-menu-content,.vjs-layout-x-large .vjs-menu-button-popup .vjs-menu .vjs-menu-content{max-height:25em}.vjs-menu-button-popup .vjs-menu.vjs-lock-showing,.vjs-workinghover .vjs-menu-button-popup.vjs-hover .vjs-menu{display:block}.video-js .vjs-menu-button-inline{transition:all .4s;overflow:hidden}.video-js .vjs-menu-button-inline:before{width:2.222222222em}.video-js .vjs-menu-button-inline.vjs-slider-active,.video-js .vjs-menu-button-inline:focus,.video-js .vjs-menu-button-inline:hover,.video-js.vjs-no-flex .vjs-menu-button-inline{width:12em}.vjs-menu-button-inline .vjs-menu{opacity:0;height:100%;width:auto;position:absolute;left:4em;top:0;padding:0;margin:0;transition:all .4s}.vjs-menu-button-inline.vjs-slider-active .vjs-menu,.vjs-menu-button-inline:focus .vjs-menu,.vjs-menu-button-inline:hover .vjs-menu{display:block;opacity:1}.vjs-no-flex .vjs-menu-button-inline .vjs-menu{display:block;opacity:1;position:relative;width:auto}.vjs-no-flex .vjs-menu-button-inline.vjs-slider-active .vjs-menu,.vjs-no-flex .vjs-menu-button-inline:focus .vjs-menu,.vjs-no-flex .vjs-menu-button-inline:hover .vjs-menu{width:auto}.vjs-menu-button-inline .vjs-menu-content{width:auto;height:100%;margin:0;overflow:hidden}.video-js .vjs-control-bar{display:none;width:100%;position:absolute;bottom:0;left:0;right:0;height:3em;background-color:#2b333f;background-color:rgba(43,51,63,.7)}.vjs-has-started .vjs-control-bar{display:flex;visibility:visible;opacity:1;transition:visibility .1s,opacity .1s}.vjs-has-started.vjs-user-inactive.vjs-playing .vjs-control-bar{visibility:visible;opacity:0;transition:visibility 1s,opacity 1s}.vjs-controls-disabled .vjs-control-bar,.vjs-error .vjs-control-bar,.vjs-using-native-controls .vjs-control-bar{display:none!important}.vjs-audio.vjs-has-started.vjs-user-inactive.vjs-playing .vjs-control-bar{opacity:1;visibility:visible}.vjs-has-started.vjs-no-flex .vjs-control-bar{display:table}.video-js .vjs-control{position:relative;text-align:center;margin:0;padding:0;height:100%;width:4em;flex:none}.vjs-button>.vjs-icon-placeholder:before{font-size:1.8em;line-height:1.67}.video-js .vjs-control:focus,.video-js .vjs-control:focus:before,.video-js .vjs-control:hover:before{text-shadow:0 0 1em #fff}.video-js .vjs-control-text{border:0;clip:rect(0 0 0 0);height:1px;overflow:hidden;padding:0;position:absolute;width:1px}.vjs-no-flex .vjs-control{display:table-cell;vertical-align:middle}.video-js .vjs-custom-control-spacer{display:none}.video-js .vjs-progress-control{cursor:pointer;flex:auto;display:flex;align-items:center;min-width:4em;touch-action:none}.video-js .vjs-progress-control.disabled{cursor:default}.vjs-live .vjs-progress-control{display:none}.vjs-liveui .vjs-progress-control{display:flex;align-items:center}.vjs-no-flex .vjs-progress-control{width:auto}.video-js .vjs-progress-holder{flex:auto;transition:all .2s;height:.3em}.video-js .vjs-progress-control .vjs-progress-holder{margin:0 10px}.video-js .vjs-progress-control:hover .vjs-progress-holder{font-size:1.6666666667em}.video-js .vjs-progress-control:hover .vjs-progress-holder.disabled{font-size:1em}.video-js .vjs-progress-holder .vjs-load-progress,.video-js .vjs-progress-holder .vjs-load-progress div,.video-js .vjs-progress-holder .vjs-play-progress{position:absolute;display:block;height:100%;margin:0;padding:0;width:0}.video-js .vjs-play-progress{background-color:#fff}.video-js .vjs-play-progress:before{font-size:.9em;position:absolute;right:-.5em;top:-.3333333333em;z-index:1}.video-js .vjs-load-progress{background:rgba(115,133,159,.5)}.video-js .vjs-load-progress div{background:rgba(115,133,159,.75)}.video-js .vjs-time-tooltip{background-color:#fff;background-color:rgba(255,255,255,.8);border-radius:.3em;color:#000;float:right;font-family:Arial,Helvetica,sans-serif;font-size:1em;padding:6px 8px 8px 8px;pointer-events:none;position:absolute;top:-3.4em;visibility:hidden;z-index:1}.video-js .vjs-progress-holder:focus .vjs-time-tooltip{display:none}.video-js .vjs-progress-control:hover .vjs-progress-holder:focus .vjs-time-tooltip,.video-js .vjs-progress-control:hover .vjs-time-tooltip{display:block;font-size:.6em;visibility:visible}.video-js .vjs-progress-control.disabled:hover .vjs-time-tooltip{font-size:1em}.video-js .vjs-progress-control .vjs-mouse-display{display:none;position:absolute;width:1px;height:100%;background-color:#000;z-index:1}.vjs-no-flex .vjs-progress-control .vjs-mouse-display{z-index:0}.video-js .vjs-progress-control:hover .vjs-mouse-display{display:block}.video-js.vjs-user-inactive .vjs-progress-control .vjs-mouse-display{visibility:hidden;opacity:0;transition:visibility 1s,opacity 1s}.video-js.vjs-user-inactive.vjs-no-flex .vjs-progress-control .vjs-mouse-display{display:none}.vjs-mouse-display .vjs-time-tooltip{color:#fff;background-color:#000;background-color:rgba(0,0,0,.8)}.video-js .vjs-slider{position:relative;cursor:pointer;padding:0;margin:0 .45em 0 .45em;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:#73859f;background-color:rgba(115,133,159,.5)}.video-js .vjs-slider.disabled{cursor:default}.video-js .vjs-slider:focus{text-shadow:0 0 1em #fff;box-shadow:0 0 1em #fff}.video-js .vjs-mute-control{cursor:pointer;flex:none}.video-js .vjs-volume-control{cursor:pointer;margin-right:1em;display:flex}.video-js .vjs-volume-control.vjs-volume-horizontal{width:5em}.video-js .vjs-volume-panel .vjs-volume-control{visibility:visible;opacity:0;width:1px;height:1px;margin-left:-1px}.video-js .vjs-volume-panel{transition:width 1s}.video-js .vjs-volume-panel .vjs-volume-control.vjs-slider-active,.video-js .vjs-volume-panel .vjs-volume-control:active,.video-js .vjs-volume-panel.vjs-hover .vjs-mute-control~.vjs-volume-control,.video-js .vjs-volume-panel.vjs-hover .vjs-volume-control,.video-js .vjs-volume-panel:active .vjs-volume-control,.video-js .vjs-volume-panel:focus .vjs-volume-control{visibility:visible;opacity:1;position:relative;transition:visibility .1s,opacity .1s,height .1s,width .1s,left 0s,top 0s}.video-js .vjs-volume-panel .vjs-volume-control.vjs-slider-active.vjs-volume-horizontal,.video-js .vjs-volume-panel .vjs-volume-control:active.vjs-volume-horizontal,.video-js .vjs-volume-panel.vjs-hover .vjs-mute-control~.vjs-volume-control.vjs-volume-horizontal,.video-js .vjs-volume-panel.vjs-hover .vjs-volume-control.vjs-volume-horizontal,.video-js .vjs-volume-panel:active .vjs-volume-control.vjs-volume-horizontal,.video-js .vjs-volume-panel:focus .vjs-volume-control.vjs-volume-horizontal{width:5em;height:3em;margin-right:0}.video-js .vjs-volume-panel .vjs-volume-control.vjs-slider-active.vjs-volume-vertical,.video-js .vjs-volume-panel .vjs-volume-control:active.vjs-volume-vertical,.video-js .vjs-volume-panel.vjs-hover .vjs-mute-control~.vjs-volume-control.vjs-volume-vertical,.video-js .vjs-volume-panel.vjs-hover .vjs-volume-control.vjs-volume-vertical,.video-js .vjs-volume-panel:active .vjs-volume-control.vjs-volume-vertical,.video-js .vjs-volume-panel:focus .vjs-volume-control.vjs-volume-vertical{left:-3.5em;transition:left 0s}.video-js .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-hover,.video-js .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-slider-active,.video-js .vjs-volume-panel.vjs-volume-panel-horizontal:active{width:10em;transition:width .1s}.video-js .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-mute-toggle-only{width:4em}.video-js .vjs-volume-panel .vjs-volume-control.vjs-volume-vertical{height:8em;width:3em;left:-3000em;transition:visibility 1s,opacity 1s,height 1s 1s,width 1s 1s,left 1s 1s,top 1s 1s}.video-js .vjs-volume-panel .vjs-volume-control.vjs-volume-horizontal{transition:visibility 1s,opacity 1s,height 1s 1s,width 1s,left 1s 1s,top 1s 1s}.video-js.vjs-no-flex .vjs-volume-panel .vjs-volume-control.vjs-volume-horizontal{width:5em;height:3em;visibility:visible;opacity:1;position:relative;transition:none}.video-js.vjs-no-flex .vjs-volume-control.vjs-volume-vertical,.video-js.vjs-no-flex .vjs-volume-panel .vjs-volume-control.vjs-volume-vertical{position:absolute;bottom:3em;left:.5em}.video-js .vjs-volume-panel{display:flex}.video-js .vjs-volume-bar{margin:1.35em .45em}.vjs-volume-bar.vjs-slider-horizontal{width:5em;height:.3em}.vjs-volume-bar.vjs-slider-vertical{width:.3em;height:5em;margin:1.35em auto}.video-js .vjs-volume-level{position:absolute;bottom:0;left:0;background-color:#fff}.video-js .vjs-volume-level:before{position:absolute;font-size:.9em}.vjs-slider-vertical .vjs-volume-level{width:.3em}.vjs-slider-vertical .vjs-volume-level:before{top:-.5em;left:-.3em}.vjs-slider-horizontal .vjs-volume-level{height:.3em}.vjs-slider-horizontal .vjs-volume-level:before{top:-.3em;right:-.5em}.video-js .vjs-volume-panel.vjs-volume-panel-vertical{width:4em}.vjs-volume-bar.vjs-slider-vertical .vjs-volume-level{height:100%}.vjs-volume-bar.vjs-slider-horizontal .vjs-volume-level{width:100%}.video-js .vjs-volume-vertical{width:3em;height:8em;bottom:8em;background-color:#2b333f;background-color:rgba(43,51,63,.7)}.video-js .vjs-volume-horizontal .vjs-menu{left:-2em}.vjs-poster{display:inline-block;vertical-align:middle;background-repeat:no-repeat;background-position:50% 50%;background-size:contain;background-color:#000;cursor:pointer;margin:0;padding:0;position:absolute;top:0;right:0;bottom:0;left:0;height:100%}.vjs-has-started .vjs-poster{display:none}.vjs-audio.vjs-has-started .vjs-poster{display:block}.vjs-using-native-controls .vjs-poster{display:none}.video-js .vjs-live-control{display:flex;align-items:flex-start;flex:auto;font-size:1em;line-height:3em}.vjs-no-flex .vjs-live-control{display:table-cell;width:auto;text-align:left}.video-js.vjs-liveui .vjs-live-control,.video-js:not(.vjs-live) .vjs-live-control{display:none}.video-js .vjs-seek-to-live-control{cursor:pointer;flex:none;display:inline-flex;height:100%;padding-left:.5em;padding-right:.5em;font-size:1em;line-height:3em;width:auto;min-width:4em}.vjs-no-flex .vjs-seek-to-live-control{display:table-cell;width:auto;text-align:left}.video-js.vjs-live:not(.vjs-liveui) .vjs-seek-to-live-control,.video-js:not(.vjs-live) .vjs-seek-to-live-control{display:none}.vjs-seek-to-live-control.vjs-control.vjs-at-live-edge{cursor:auto}.vjs-seek-to-live-control .vjs-icon-placeholder{margin-right:.5em;color:#888}.vjs-seek-to-live-control.vjs-control.vjs-at-live-edge .vjs-icon-placeholder{color:red}.video-js .vjs-time-control{flex:none;font-size:1em;line-height:3em;min-width:2em;width:auto;padding-left:1em;padding-right:1em}.vjs-live .vjs-time-control{display:none}.video-js .vjs-current-time,.vjs-no-flex .vjs-current-time{display:none}.video-js .vjs-duration,.vjs-no-flex .vjs-duration{display:none}.vjs-time-divider{display:none;line-height:3em}.vjs-live .vjs-time-divider{display:none}.video-js .vjs-play-control{cursor:pointer}.video-js .vjs-play-control .vjs-icon-placeholder{flex:none}.vjs-text-track-display{position:absolute;bottom:3em;left:0;right:0;top:0;pointer-events:none}.video-js.vjs-user-inactive.vjs-playing .vjs-text-track-display{bottom:1em}.video-js .vjs-text-track{font-size:1.4em;text-align:center;margin-bottom:.1em}.vjs-subtitles{color:#fff}.vjs-captions{color:#fc6}.vjs-tt-cue{display:block}video::-webkit-media-text-track-display{transform:translateY(-3em)}.video-js.vjs-user-inactive.vjs-playing video::-webkit-media-text-track-display{transform:translateY(-1.5em)}.video-js .vjs-picture-in-picture-control{cursor:pointer;flex:none}.video-js .vjs-fullscreen-control{cursor:pointer;flex:none}.vjs-playback-rate .vjs-playback-rate-value,.vjs-playback-rate>.vjs-menu-button{position:absolute;top:0;left:0;width:100%;height:100%}.vjs-playback-rate .vjs-playback-rate-value{pointer-events:none;font-size:1.5em;line-height:2;text-align:center}.vjs-playback-rate .vjs-menu{width:4em;left:0}.vjs-error .vjs-error-display .vjs-modal-dialog-content{font-size:1.4em;text-align:center}.vjs-error .vjs-error-display:before{color:#fff;content:"X";font-family:Arial,Helvetica,sans-serif;font-size:4em;left:0;line-height:1;margin-top:-.5em;position:absolute;text-shadow:.05em .05em .1em #000;text-align:center;top:50%;vertical-align:middle;width:100%}.vjs-loading-spinner{display:none;position:absolute;top:50%;left:50%;margin:-25px 0 0 -25px;opacity:.85;text-align:left;border:6px solid rgba(43,51,63,.7);box-sizing:border-box;background-clip:padding-box;width:50px;height:50px;border-radius:25px;visibility:hidden}.vjs-seeking .vjs-loading-spinner,.vjs-waiting .vjs-loading-spinner{display:block;-webkit-animation:vjs-spinner-show 0s linear .3s forwards;animation:vjs-spinner-show 0s linear .3s forwards}.vjs-loading-spinner:after,.vjs-loading-spinner:before{content:"";position:absolute;margin:-6px;box-sizing:inherit;width:inherit;height:inherit;border-radius:inherit;opacity:1;border:inherit;border-color:transparent;border-top-color:#fff}.vjs-seeking .vjs-loading-spinner:after,.vjs-seeking .vjs-loading-spinner:before,.vjs-waiting .vjs-loading-spinner:after,.vjs-waiting .vjs-loading-spinner:before{-webkit-animation:vjs-spinner-spin 1.1s cubic-bezier(.6,.2,0,.8) infinite,vjs-spinner-fade 1.1s linear infinite;animation:vjs-spinner-spin 1.1s cubic-bezier(.6,.2,0,.8) infinite,vjs-spinner-fade 1.1s linear infinite}.vjs-seeking .vjs-loading-spinner:before,.vjs-waiting .vjs-loading-spinner:before{border-top-color:#fff}.vjs-seeking .vjs-loading-spinner:after,.vjs-waiting .vjs-loading-spinner:after{border-top-color:#fff;-webkit-animation-delay:.44s;animation-delay:.44s}@keyframes vjs-spinner-show{to{visibility:visible}}@-webkit-keyframes vjs-spinner-show{to{visibility:visible}}@keyframes vjs-spinner-spin{100%{transform:rotate(360deg)}}@-webkit-keyframes vjs-spinner-spin{100%{-webkit-transform:rotate(360deg)}}@keyframes vjs-spinner-fade{0%{border-top-color:#73859f}20%{border-top-color:#73859f}35%{border-top-color:#fff}60%{border-top-color:#73859f}100%{border-top-color:#73859f}}@-webkit-keyframes vjs-spinner-fade{0%{border-top-color:#73859f}20%{border-top-color:#73859f}35%{border-top-color:#fff}60%{border-top-color:#73859f}100%{border-top-color:#73859f}}.vjs-chapters-button .vjs-menu ul{width:24em}.video-js .vjs-subs-caps-button+.vjs-menu .vjs-captions-menu-item .vjs-menu-item-text .vjs-icon-placeholder{vertical-align:middle;display:inline-block;margin-bottom:-.1em}.video-js .vjs-subs-caps-button+.vjs-menu .vjs-captions-menu-item .vjs-menu-item-text .vjs-icon-placeholder:before{font-family:VideoJS;content:"";font-size:1.5em;line-height:inherit}.video-js .vjs-audio-button+.vjs-menu .vjs-main-desc-menu-item .vjs-menu-item-text .vjs-icon-placeholder{vertical-align:middle;display:inline-block;margin-bottom:-.1em}.video-js .vjs-audio-button+.vjs-menu .vjs-main-desc-menu-item .vjs-menu-item-text .vjs-icon-placeholder:before{font-family:VideoJS;content:" ";font-size:1.5em;line-height:inherit}.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-audio-button,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-captions-button,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-chapters-button,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-current-time,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-descriptions-button,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-duration,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-playback-rate,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-remaining-time,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-subtitles-button,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-time-divider,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-volume-control,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-audio-button,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-captions-button,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-chapters-button,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-current-time,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-descriptions-button,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-duration,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-playback-rate,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-remaining-time,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-subtitles-button,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-time-divider,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-volume-control,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-audio-button,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-captions-button,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-chapters-button,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-current-time,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-descriptions-button,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-duration,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-playback-rate,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-remaining-time,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-subtitles-button,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-time-divider,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-volume-control{display:none}.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-slider-active,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-volume-panel.vjs-volume-panel-horizontal:active,.video-js:not(.vjs-fullscreen).vjs-layout-small .vjs-volume-panel.vjs-volume-panel-horizontal:hover,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-slider-active,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-volume-panel.vjs-volume-panel-horizontal:active,.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-volume-panel.vjs-volume-panel-horizontal:hover,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-slider-active,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-volume-panel.vjs-volume-panel-horizontal:active,.video-js:not(.vjs-fullscreen).vjs-layout-x-small .vjs-volume-panel.vjs-volume-panel-horizontal:hover{width:auto;width:initial}.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-subs-caps-button,.video-js:not(.vjs-fullscreen).vjs-layout-x-small:not(.vjs-live) .vjs-subs-caps-button,.video-js:not(.vjs-fullscreen).vjs-layout-x-small:not(.vjs-liveui) .vjs-subs-caps-button{display:none}.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-custom-control-spacer,.video-js:not(.vjs-fullscreen).vjs-layout-x-small.vjs-liveui .vjs-custom-control-spacer{flex:auto;display:block}.video-js:not(.vjs-fullscreen).vjs-layout-tiny.vjs-no-flex .vjs-custom-control-spacer,.video-js:not(.vjs-fullscreen).vjs-layout-x-small.vjs-liveui.vjs-no-flex .vjs-custom-control-spacer{width:auto}.video-js:not(.vjs-fullscreen).vjs-layout-tiny .vjs-progress-control,.video-js:not(.vjs-fullscreen).vjs-layout-x-small.vjs-liveui .vjs-progress-control{display:none}.vjs-modal-dialog.vjs-text-track-settings{background-color:#2b333f;background-color:rgba(43,51,63,.75);color:#fff;height:70%}.vjs-text-track-settings .vjs-modal-dialog-content{display:table}.vjs-text-track-settings .vjs-track-settings-colors,.vjs-text-track-settings .vjs-track-settings-controls,.vjs-text-track-settings .vjs-track-settings-font{display:table-cell}.vjs-text-track-settings .vjs-track-settings-controls{text-align:right;vertical-align:bottom}@supports (display:grid){.vjs-text-track-settings .vjs-modal-dialog-content{display:grid;grid-template-columns:1fr 1fr;grid-template-rows:1fr;padding:20px 24px 0 24px}.vjs-track-settings-controls .vjs-default-button{margin-bottom:20px}.vjs-text-track-settings .vjs-track-settings-controls{grid-column:1/-1}.vjs-layout-small .vjs-text-track-settings .vjs-modal-dialog-content,.vjs-layout-tiny .vjs-text-track-settings .vjs-modal-dialog-content,.vjs-layout-x-small .vjs-text-track-settings .vjs-modal-dialog-content{grid-template-columns:1fr}}.vjs-track-setting>select{margin-right:1em;margin-bottom:.5em}.vjs-text-track-settings fieldset{margin:5px;padding:3px;border:none}.vjs-text-track-settings fieldset span{display:inline-block}.vjs-text-track-settings fieldset span>select{max-width:7.3em}.vjs-text-track-settings legend{color:#fff;margin:0 0 5px 0}.vjs-text-track-settings .vjs-label{position:absolute;clip:rect(1px 1px 1px 1px);clip:rect(1px,1px,1px,1px);display:block;margin:0 0 5px 0;padding:0;border:0;height:1px;width:1px;overflow:hidden}.vjs-track-settings-controls button:active,.vjs-track-settings-controls button:focus{outline-style:solid;outline-width:medium;background-image:linear-gradient(0deg,#fff 88%,#73859f 100%)}.vjs-track-settings-controls button:hover{color:rgba(43,51,63,.75)}.vjs-track-settings-controls button{background-color:#fff;background-image:linear-gradient(-180deg,#fff 88%,#73859f 100%);color:#2b333f;cursor:pointer;border-radius:2px}.vjs-track-settings-controls .vjs-default-button{margin-right:1em}@media print{.video-js>:not(.vjs-tech):not(.vjs-poster){visibility:hidden}}.vjs-resize-manager{position:absolute;top:0;left:0;width:100%;height:100%;border:none;z-index:-1000}.js-focus-visible .video-js :focus:not(.focus-visible){outline:0;background:0 0}.video-js .vjs-menu :focus:not(:focus-visible),.video-js :focus:not(:focus-visible){outline:0;background:0 0} \ No newline at end of file diff --git a/assets/js/global.js b/assets/js/global.js deleted file mode 100644 index efb447fb..00000000 --- a/assets/js/global.js +++ /dev/null @@ -1,3 +0,0 @@ -// Disable Web Workers. Fixes Video.js CSP violation (created by `new Worker(objURL)`): -// Refused to create a worker from 'blob:http://host/id' because it violates the following Content Security Policy directive: "worker-src 'self'". -window.Worker = undefined; diff --git a/assets/js/video.min.js b/assets/js/video.min.js index ed10c74e..890a3c84 100644 --- a/assets/js/video.min.js +++ b/assets/js/video.min.js @@ -1,6 +1,6 @@ /** * @license - * Video.js 7.6.6 + * Video.js 7.10.2 * Copyright Brightcove, Inc. * Available under Apache License Version 2.0 * @@ -9,13 +9,18 @@ * Available under Apache License Version 2.0 * */ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("global/window"),require("global/document")):"function"==typeof define&&define.amd?define(["global/window","global/document"],t):(e=e||self).videojs=t(e.window,e.document)}(this,function(v,h){v=v&&v.hasOwnProperty("default")?v.default:v,h=h&&h.hasOwnProperty("default")?h.default:h;var d="7.6.6",u=[],e=function(s,o){return function(e,t,i){var n=o.levels[t],r=new RegExp("^("+n+")$");if("log"!==e&&i.unshift(e.toUpperCase()+":"),i.unshift(s+":"),u&&u.push([].concat(i)),v.console){var a=v.console[e];a||"debug"!==e||(a=v.console.info||v.console.log),a&&n&&r.test(e)&&a[Array.isArray(i)?"apply":"call"](v.console,i)}}};var p=function t(i){function n(){for(var e=arguments.length,t=new Array(e),i=0;i',i=n.firstChild,n.setAttribute("style","display:none; position:absolute;"),h.body.appendChild(n));for(var a={},s=0;sx',e=t.firstChild.href}return e}function Mt(e){if("string"==typeof e){var t=/^(\/?)([\s\S]*?)((?:\.{1,2}|[^\/]+?)(\.([^\.\/\?]+)))(?:[\/]*|[\?].*)$/.exec(e);if(t)return t.pop().toLowerCase()}return""}function Nt(e){var t=v.location,i=Ut(e);return(":"===i.protocol?t.protocol:i.protocol)+i.host!==t.protocol+t.host}var Bt=function(n){function e(e){var t;void 0===e&&(e=[]);for(var i=e.length-1;0<=i;i--)if(e[i].enabled){xt(e,e[i]);break}return(t=n.call(this,e)||this).changing_=!1,t}De(e,n);var t=e.prototype;return t.addTrack=function(e){var t=this;e.enabled&&xt(this,e),n.prototype.addTrack.call(this,e),e.addEventListener&&(e.enabledChange_=function(){t.changing_||(t.changing_=!0,xt(t,e),t.changing_=!1,t.trigger("change"))},e.addEventListener("enabledchange",e.enabledChange_))},t.removeTrack=function(e){n.prototype.removeTrack.call(this,e),e.removeEventListener&&e.enabledChange_&&(e.removeEventListener("enabledchange",e.enabledChange_),e.enabledChange_=null)},e}(Lt),jt=function(n){function e(e){var t;void 0===e&&(e=[]);for(var i=e.length-1;0<=i;i--)if(e[i].selected){Dt(e,e[i]);break}return(t=n.call(this,e)||this).changing_=!1,Object.defineProperty(Me(t),"selectedIndex",{get:function(){for(var e=0;e>0},ToUint32:function(e){return this.ToNumber(e)>>>0},ToUint16:function(e){var t=this.ToNumber(e);return zi(t)||0===t||!Gi(t)?0:function(e,t){var i=e%t;return Math.floor(0<=i?i:i+t)}(Pi(t)*Math.floor(Math.abs(t)),65536)},ToString:function(e){return nn(e)},ToObject:function(e){return this.CheckObjectCoercible(e),en(e)},CheckObjectCoercible:function(e,t){if(null==e)throw new tn(t||"Cannot call method on "+e);return e},IsCallable:Li,SameValue:function(e,t){return e===t?0!==e||1/e==1/t:zi(e)&&zi(t)},Type:function(e){return null===e?"Null":"undefined"==typeof e?"Undefined":"function"==typeof e||"object"==typeof e?"Object":"number"==typeof e?"Number":"boolean"==typeof e?"Boolean":"string"==typeof e?"String":void 0},IsPropertyDescriptor:function(e){if("Object"!==this.Type(e))return!1;var t={"[[Configurable]]":!0,"[[Enumerable]]":!0,"[[Get]]":!0,"[[Set]]":!0,"[[Value]]":!0,"[[Writable]]":!0};for(var i in e)if(Hi(e,i)&&!t[i])return!1;var n=Hi(e,"[[Value]]"),r=Hi(e,"[[Get]]")||Hi(e,"[[Set]]");if(n&&r)throw new tn("Property Descriptors may not be both accessor and data descriptors");return!0},IsAccessorDescriptor:function(e){return"undefined"!=typeof e&&(Ai(this,"Property Descriptor","Desc",e),!(!Hi(e,"[[Get]]")&&!Hi(e,"[[Set]]")))},IsDataDescriptor:function(e){return"undefined"!=typeof e&&(Ai(this,"Property Descriptor","Desc",e),!(!Hi(e,"[[Value]]")&&!Hi(e,"[[Writable]]")))},IsGenericDescriptor:function(e){return"undefined"!=typeof e&&(Ai(this,"Property Descriptor","Desc",e),!this.IsAccessorDescriptor(e)&&!this.IsDataDescriptor(e))},FromPropertyDescriptor:function(e){if("undefined"==typeof e)return e;if(Ai(this,"Property Descriptor","Desc",e),this.IsDataDescriptor(e))return{value:e["[[Value]]"],writable:!!e["[[Writable]]"],enumerable:!!e["[[Enumerable]]"],configurable:!!e["[[Configurable]]"]};if(this.IsAccessorDescriptor(e))return{get:e["[[Get]]"],set:e["[[Set]]"],enumerable:!!e["[[Enumerable]]"],configurable:!!e["[[Configurable]]"]};throw new tn("FromPropertyDescriptor must be called with a fully populated Property Descriptor")},ToPropertyDescriptor:function(e){if("Object"!==this.Type(e))throw new tn("ToPropertyDescriptor requires an object");var t={};if(Hi(e,"enumerable")&&(t["[[Enumerable]]"]=this.ToBoolean(e.enumerable)),Hi(e,"configurable")&&(t["[[Configurable]]"]=this.ToBoolean(e.configurable)),Hi(e,"value")&&(t["[[Value]]"]=e.value),Hi(e,"writable")&&(t["[[Writable]]"]=this.ToBoolean(e.writable)),Hi(e,"get")){var i=e.get;if("undefined"!=typeof i&&!this.IsCallable(i))throw new TypeError("getter must be a function");t["[[Get]]"]=i}if(Hi(e,"set")){var n=e.set;if("undefined"!=typeof n&&!this.IsCallable(n))throw new tn("setter must be a function");t["[[Set]]"]=n}if((Hi(t,"[[Get]]")||Hi(t,"[[Set]]"))&&(Hi(t,"[[Value]]")||Hi(t,"[[Writable]]")))throw new tn("Invalid property descriptor. Cannot both specify accessors and a value or writable attribute");return t}},an=ti.call(Function.call,String.prototype.replace),sn=/^[\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028\u2029\uFEFF]+/,on=/[\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028\u2029\uFEFF]+$/,un=ti.call(Function.call,Di());Ri(un,{getPolyfill:Di,implementation:xi,shim:function(){var e=Di();return Ri(String.prototype,{trim:e},{trim:function(){return String.prototype.trim!==e}}),e}});var ln=un,cn=Object.prototype.toString,hn=Object.prototype.hasOwnProperty,dn=function(e,t,i){if(!Li(t))throw new TypeError("iterator must be a function");var n;3<=arguments.length&&(n=i),"[object Array]"===cn.call(e)?function(e,t,i){for(var n=0,r=e.length;n=e?t.push(r):r.startTime===r.endTime&&r.startTime<=e&&r.startTime+.5>=e&&t.push(r)}if(o=!1,t.length!==this.activeCues_.length)o=!0;else for(var a=0;a","‎":"‎","‏":"‏"," ":" "},Mn={c:"span",i:"i",b:"b",u:"u",ruby:"ruby",rt:"rt",v:"span",lang:"span"},Nn={v:"title",lang:"lang"},Bn={rt:"ruby"};function jn(a,i){function e(){if(!i)return null;var e,t=i.match(/^([^<]*)(<[^>]*>?)?/);return e=t[1]?t[1]:t[2],i=i.substr(e.length),e}function t(e){return Rn[e]}function n(e){for(;f=e.match(/&(amp|lt|gt|lrm|rlm|nbsp);/);)e=e.replace(f[0],t);return e}function r(e,t){var i=Mn[e];if(!i)return null;var n=a.document.createElement(i);n.localName=i;var r=Nn[e];return r&&t&&(n[r]=t.trim()),n}for(var s,o,u,l=a.document.createElement("div"),c=l,h=[];null!==(s=e());)if("<"!==s[0])c.appendChild(a.document.createTextNode(n(s)));else{if("/"===s[1]){h.length&&h[h.length-1]===s.substr(2).replace(">","")&&(h.pop(),c=c.parentNode);continue}var d,p=On(s.substr(1,s.length-2));if(p){d=a.document.createProcessingInstruction("timestamp",p),c.appendChild(d);continue}var f=s.match(/^<([^.\s/0-9>]+)(\.[^\s\\>]+)?([^>\\]+)?(\\?)>?$/);if(!f)continue;if(!(d=r(f[1],f[3])))continue;if(o=c,Bn[(u=d).localName]&&Bn[u.localName]!==o.localName)continue;f[2]&&(d.className=f[2].substr(1).replace("."," ")),h.push(f[1]),c.appendChild(d),c=d}return l}var Fn=[[1470,1470],[1472,1472],[1475,1475],[1478,1478],[1488,1514],[1520,1524],[1544,1544],[1547,1547],[1549,1549],[1563,1563],[1566,1610],[1645,1647],[1649,1749],[1765,1766],[1774,1775],[1786,1805],[1807,1808],[1810,1839],[1869,1957],[1969,1969],[1984,2026],[2036,2037],[2042,2042],[2048,2069],[2074,2074],[2084,2084],[2088,2088],[2096,2110],[2112,2136],[2142,2142],[2208,2208],[2210,2220],[8207,8207],[64285,64285],[64287,64296],[64298,64310],[64312,64316],[64318,64318],[64320,64321],[64323,64324],[64326,64449],[64467,64829],[64848,64911],[64914,64967],[65008,65020],[65136,65140],[65142,65276],[67584,67589],[67592,67592],[67594,67637],[67639,67640],[67644,67644],[67647,67669],[67671,67679],[67840,67867],[67872,67897],[67903,67903],[67968,68023],[68030,68031],[68096,68096],[68112,68115],[68117,68119],[68121,68147],[68160,68167],[68176,68184],[68192,68223],[68352,68405],[68416,68437],[68440,68466],[68472,68479],[68608,68680],[126464,126467],[126469,126495],[126497,126498],[126500,126500],[126503,126503],[126505,126514],[126516,126519],[126521,126521],[126523,126523],[126530,126530],[126535,126535],[126537,126537],[126539,126539],[126541,126543],[126545,126546],[126548,126548],[126551,126551],[126553,126553],[126555,126555],[126557,126557],[126559,126559],[126561,126562],[126564,126564],[126567,126570],[126572,126578],[126580,126583],[126585,126588],[126590,126590],[126592,126601],[126603,126619],[126625,126627],[126629,126633],[126635,126651],[1114109,1114109]];function Hn(e){for(var t=0;t=i[0]&&e<=i[1])return!0}return!1}function Vn(){}function qn(e,t,i){Vn.call(this),this.cue=t,this.cueDiv=jn(e,t.text);var n={color:"rgba(255, 255, 255, 1)",backgroundColor:"rgba(0, 0, 0, 0.8)",position:"relative",left:0,right:0,top:0,bottom:0,display:"inline",writingMode:""===t.vertical?"horizontal-tb":"lr"===t.vertical?"vertical-lr":"vertical-rl",unicodeBidi:"plaintext"};this.applyStyles(n,this.cueDiv),this.div=e.document.createElement("div"),n={direction:function(e){var t=[],i="";if(!e||!e.childNodes)return"ltr";function r(e,t){for(var i=t.childNodes.length-1;0<=i;i--)e.push(t.childNodes[i])}function a(e){if(!e||!e.length)return null;var t=e.pop(),i=t.textContent||t.innerText;if(i){var n=i.match(/^.*(\n|\r)/);return n?n[e.length=0]:i}return"ruby"===t.tagName?a(e):t.childNodes?(r(e,t),a(e)):void 0}for(r(t,e);i=a(t);)for(var n=0;nh&&(c=c<0?-1:1,c*=Math.ceil(h/l)*l),r<0&&(c+=""===n.vertical?o.height:o.width,a=a.reverse()),i.move(d,c)}else{var p=i.lineHeight/o.height*100;switch(n.lineAlign){case"middle":r-=p/2;break;case"end":r-=p}switch(n.vertical){case"":t.applyStyles({top:t.formatStyle(r,"%")});break;case"rl":t.applyStyles({left:t.formatStyle(r,"%")});break;case"lr":t.applyStyles({right:t.formatStyle(r,"%")})}a=["+y","-x","+x","-y"],i=new Wn(t)}var f=function(e,t){for(var i,n=new Wn(e),r=1,a=0;ae.left&&this.tope.top},Wn.prototype.overlapsAny=function(e){for(var t=0;t=e.top&&this.bottom<=e.bottom&&this.left>=e.left&&this.right<=e.right},Wn.prototype.overlapsOppositeAxis=function(e,t){switch(t){case"+x":return this.lefte.right;case"+y":return this.tope.bottom}},Wn.prototype.intersectPercentage=function(e){return Math.max(0,Math.min(this.right,e.right)-Math.max(this.left,e.left))*Math.max(0,Math.min(this.bottom,e.bottom)-Math.max(this.top,e.top))/(this.height*this.width)},Wn.prototype.toCSSCompatValues=function(e){return{top:this.top-e.top,bottom:e.bottom-this.bottom,left:this.left-e.left,right:e.right-this.right,height:this.height,width:this.width}},Wn.getSimpleBoxPosition=function(e){var t=e.div?e.div.offsetHeight:e.tagName?e.offsetHeight:0,i=e.div?e.div.offsetWidth:e.tagName?e.offsetWidth:0,n=e.div?e.div.offsetTop:e.tagName?e.offsetTop:0;return{left:(e=e.div?e.div.getBoundingClientRect():e.tagName?e.getBoundingClientRect():e).left,right:e.right,top:e.top||n,height:e.height||t,bottom:e.bottom||n+(e.height||t),width:e.width||i}},$n.StringDecoder=function(){return{decode:function(e){if(!e)return"";if("string"!=typeof e)throw new Error("Error - expected string data.");return decodeURIComponent(encodeURIComponent(e))}}},$n.convertCueToDOMTree=function(e,t){return e&&t?jn(e,t):null};$n.processCues=function(n,r,e){if(!n||!r||!e)return null;for(;e.firstChild;)e.removeChild(e.firstChild);var a=n.document.createElement("div");if(a.style.position="absolute",a.style.left="0",a.style.right="0",a.style.top="0",a.style.bottom="0",a.style.margin="1.5%",e.appendChild(a),function(e){for(var t=0;t