From 48cbe45a9d3212a6f32551741213b028700d59b2 Mon Sep 17 00:00:00 2001 From: edumoreira1506 Date: Wed, 20 Nov 2019 15:59:07 -0300 Subject: [PATCH 001/165] 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 58c1a68ad9023581555b4d9c943dfd447b5c92bf Mon Sep 17 00:00:00 2001 From: Jorge Maldonado Ventura Date: Sat, 4 Jan 2020 15:27:45 +0100 Subject: [PATCH 002/165] 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 648cc0f00675f1a98aed78147e532b1ae164a0e3 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Fri, 24 Jan 2020 17:02:28 -0500 Subject: [PATCH 003/165] Refactor signature extraction --- src/invidious.cr | 2 +- src/invidious/helpers/signatures.cr | 56 +++++++++++------------------ 2 files changed, 21 insertions(+), 37 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index 8340ebab..f5fe4b3d 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -207,7 +207,7 @@ spawn do end end -decrypt_function = [] of {name: String, value: Int32} +decrypt_function = [] of {SigProc, Int32} spawn do update_decrypt_function do |function| decrypt_function = function diff --git a/src/invidious/helpers/signatures.cr b/src/invidious/helpers/signatures.cr index 1d238576..ab864f03 100644 --- a/src/invidious/helpers/signatures.cr +++ b/src/invidious/helpers/signatures.cr @@ -1,69 +1,53 @@ +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-.{9}\/en_US\/base.js)"/).not_nil!["url"] + 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\(a\){a=a\.split\(""\)/m).not_nil!["name"] - function_body = player.match(/^#{Regex.escape(function_name)}=function\(a\){(?[^}]+)}/m).not_nil!["body"] + 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 => String + 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" + operations[op_name] = ->(a : Array(String), b : Int32) { a.reverse } when "{a.splice(0,b)" - operations[op_name] = "b" + operations[op_name] = ->(a : Array(String), b : Int32) { a.delete_at(0..(b - 1)); a } else - operations[op_name] = "c" + 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 {name: String, value: Int32} + 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(/\(a,(?[\d]+)\)/).not_nil!["value"].to_i + value = function.match(/\(\w,(?[\d]+)\)/).not_nil!["value"].to_i - decrypt_function << {name: operations[op_name], value: value} + decrypt_function << {operations[op_name], value} end return decrypt_function end -def decrypt_signature(fmt, code) - if !fmt["s"]? - return "" +def decrypt_signature(fmt, op) + return "" if !fmt["s"]? || !fmt["sp"]? + + sp = fmt["sp"] + sig = fmt["s"].split("") + op.each do |proc, value| + sig = proc.call(sig, value) end - a = fmt["s"] - a = a.split("") - - code.each do |item| - case item[:name] - when "a" - a.reverse! - when "b" - a.delete_at(0..(item[:value] - 1)) - when "c" - a = splice(a, item[:value]) - end - end - - signature = a.join("") - return "&#{fmt["sp"]?}=#{signature}" -end - -def splice(a, b) - c = a[0] - a[0] = a[b % a.size] - a[b % a.size] = c - return a + return "&#{sp}=#{sig.join("")}" end From b80d34612afda3ebb64891a5754cdd877b330862 Mon Sep 17 00:00:00 2001 From: Outvi V <19144373+outloudvi@users.noreply.github.com> Date: Mon, 27 Jan 2020 13:01:53 +0800 Subject: [PATCH 004/165] Update zh-CN translation --- locales/zh-CN.json | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/locales/zh-CN.json b/locales/zh-CN.json index f8c16ea9..fe12c65e 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -1,7 +1,7 @@ { - "`x` subscribers": "`x` 订阅者", - "`x` videos": "`x` 视频", - "`x` playlists": "", + "`x` subscribers": "`x` 位订阅者", + "`x` videos": "`x` 个视频", + "`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: ": "在订阅频道的视频默认显示注释?", @@ -129,15 +129,15 @@ "Trending": "时下流行", "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": "显示注释", @@ -325,12 +325,12 @@ "%A %B %-d, %Y": "%Y年%-m月%-d日 %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 a2c5211b20ac8a6a4f488322c155b3046fd9fd86 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Fri, 31 Jan 2020 20:13:05 -0500 Subject: [PATCH 005/165] Check /browse_ajax for channel blocks --- src/invidious/helpers/jobs.cr | 169 +++++++++++++++++----------------- 1 file changed, 82 insertions(+), 87 deletions(-) diff --git a/src/invidious/helpers/jobs.cr b/src/invidious/helpers/jobs.cr index f368d6df..609e53c9 100644 --- a/src/invidious/helpers/jobs.cr +++ b/src/invidious/helpers/jobs.cr @@ -236,114 +236,109 @@ end def bypass_captcha(captcha_key, logger) loop do begin - response = YT_POOL.client &.get("/watch?v=CvFH_6DNRCY&gl=US&hl=en&disable_polymer=1&has_verified=1&bpctr=9999999999") - 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"] + {"/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| + 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) + form = html.xpath_node(%(//form[@action="/das_captcha"])).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 + inputs = {} of String => String + form.xpath_nodes(%(.//input[@name])).map do |node| + inputs[node["name"]] = node["value"] + end - headers = response.cookies.add_request_headers(HTTP::Headers.new) + headers = response.cookies.add_request_headers(HTTP::Headers.new) - response = JSON.parse(HTTP::Client.post("https://api.anti-captcha.com/createTask", body: { - "clientKey" => CONFIG.captcha_key, - "task" => { - "type" => "NoCaptchaTaskProxyless", - # "type" => "NoCaptchaTask", - "websiteURL" => "https://www.youtube.com/watch?v=CvFH_6DNRCY&gl=US&hl=en&disable_polymer=1&has_verified=1&bpctr=9999999999", - "websiteKey" => site_key, - # "proxyType" => "http", - # "proxyAddress" => CONFIG.proxy_address, - # "proxyPort" => CONFIG.proxy_port, - # "proxyLogin" => CONFIG.proxy_user, - # "proxyPassword" => CONFIG.proxy_pass, - # "userAgent" => "User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36", - }, - }.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: { + response = JSON.parse(HTTP::Client.post("https://api.anti-captcha.com/createTask", body: { "clientKey" => CONFIG.captcha_key, - "taskId" => task_id, + "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, + }, }.to_json).body) - if response["status"]?.try &.== "ready" - break - elsif response["errorId"]?.try &.as_i != 0 - raise response["errorDescription"].as_s + if response["error"]? + raise response["error"].as_s end - end - inputs["g-recaptcha-response"] = response["solution"]["gRecaptchaResponse"].as_s - response = YT_POOL.client &.post("/das_captcha", headers, form: inputs) + task_id = response["taskId"].as_i - 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) + loop do + sleep 10.seconds - 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"] + response = JSON.parse(HTTP::Client.post("https://api.anti-captcha.com/getTaskResult", body: { + "clientKey" => CONFIG.captcha_key, + "taskId" => task_id, + }.to_json).body) - inputs = {} of String => String - form.xpath_nodes(%(.//input[@name])).map do |node| - inputs[node["name"]] = node["value"] - end + if response["status"]?.try &.== "ready" + break + elsif response["errorId"]?.try &.as_i != 0 + raise response["errorDescription"].as_s + end + 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) + inputs["g-recaptcha-response"] = response["solution"]["gRecaptchaResponse"].as_s + response = YT_POOL.client &.post("/das_captcha", headers, form: inputs) - if response["error"]? - raise response["error"].as_s - end + 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) - task_id = response["taskId"].as_i + 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"] - loop do - sleep 10.seconds + 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/getTaskResult", body: { + response = JSON.parse(HTTP::Client.post("https://api.anti-captcha.com/createTask", body: { "clientKey" => CONFIG.captcha_key, - "taskId" => task_id, + "task" => { + "type" => "NoCaptchaTaskProxyless", + "websiteURL" => location.to_s, + "websiteKey" => site_key, + }, }.to_json).body) - if response["status"]?.try &.== "ready" - break - elsif response["errorId"]?.try &.as_i != 0 - raise response["errorDescription"].as_s + 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 - - 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 b56e493d92157dcddbc65b2eaa3d93bb758e8544 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Sat, 1 Feb 2020 11:12:24 -0500 Subject: [PATCH 006/165] Remove frameborder from community embeds --- src/invidious/comments.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr index 2d7bc1cf..1d32d1f4 100644 --- a/src/invidious/comments.cr +++ b/src/invidious/comments.cr @@ -347,7 +347,7 @@ def template_youtube_comments(comments, locale, thin_mode) END_HTML else html << <<-END_HTML - + END_HTML end From 9841f74adc94a4845aea6aace8d2408c3255dfa7 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Sat, 1 Feb 2020 12:14:37 -0500 Subject: [PATCH 007/165] Add handling for comments with no content --- src/invidious/channels.cr | 8 +++----- src/invidious/comments.cr | 4 ++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/invidious/channels.cr b/src/invidious/channels.cr index 7cd1bef1..cddeed39 100644 --- a/src/invidious/channels.cr +++ b/src/invidious/channels.cr @@ -628,15 +628,13 @@ def fetch_channel_community(ucid, continuation, locale, config, kemal_config, fo post = post["backstagePostThreadRenderer"]?.try &.["post"]["backstagePostRenderer"]? || post["commentThreadRenderer"]?.try &.["comment"]["commentRenderer"]? - if !post - next - end + next if !post if !post["contentText"]? content_html = "" else - content_html = post["contentText"]["simpleText"]?.try &.as_s.rchop('\ufeff').try { |block| HTML.escape(block) }.to_s || - content_to_comment_html(post["contentText"]["runs"].as_a).try &.to_s || "" + 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 author = post["authorText"]?.try &.["simpleText"]? || "" diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr index 1d32d1f4..9e4d3866 100644 --- a/src/invidious/comments.cr +++ b/src/invidious/comments.cr @@ -150,8 +150,8 @@ 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 { |block| HTML.escape(block) }.to_s || - content_to_comment_html(node_comment["contentText"]["runs"].as_a).try &.to_s || "" + 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 } || "" author = node_comment["authorText"]?.try &.["simpleText"]? || "" json.field "author", author From dd9f1024f400cf35fcc7d2fbaec6bb45828e5e60 Mon Sep 17 00:00:00 2001 From: Jorge Maldonado Ventura Date: Sat, 1 Feb 2020 19:25:03 +0100 Subject: [PATCH 008/165] Remove invalid HTML from embed player --- 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 79788b1e..eecc0868 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 e3c10d779d315adc630e08005b6bdbdce32f7446 Mon Sep 17 00:00:00 2001 From: Leon Klingele Date: Tue, 4 Feb 2020 15:50:28 +0100 Subject: [PATCH 009/165] Add support to read config from environment variable Try to read app config from the "INVIDIOUS_CONFIG" environment variable. If the variable is undefined, read config from config.yml file as before. Required by https://github.com/omarroth/invidious/pull/1015 et al. --- src/invidious.cr | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index f5fe4b3d..a4584d4b 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -28,8 +28,11 @@ require "protodec/utils" require "./invidious/helpers/*" require "./invidious/*" -CONFIG = Config.from_yaml(File.read("config/config.yml")) -HMAC_KEY = CONFIG.hmac_key || Random::Secure.hex(32) +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) PG_URL = URI.new( scheme: "postgres", From 3cde5e28a8d946c1110697946b180027204b5d2c Mon Sep 17 00:00:00 2001 From: Leon Klingele Date: Mon, 3 Feb 2020 23:19:18 +0100 Subject: [PATCH 010/165] Add support to run on Kubernetes, add Helm chart See relevant README.md for more details. --- kubernetes/.gitignore | 1 + kubernetes/Chart.lock | 6 ++++ kubernetes/Chart.yaml | 22 ++++++++++++ kubernetes/README.md | 42 ++++++++++++++++++++++ kubernetes/templates/_helpers.tpl | 16 +++++++++ kubernetes/templates/configmap.yaml | 11 ++++++ kubernetes/templates/deployment.yaml | 53 ++++++++++++++++++++++++++++ kubernetes/templates/hpa.yaml | 18 ++++++++++ kubernetes/templates/service.yaml | 16 +++++++++ kubernetes/values.yaml | 51 ++++++++++++++++++++++++++ 10 files changed, 236 insertions(+) create mode 100644 kubernetes/.gitignore create mode 100644 kubernetes/Chart.lock create mode 100644 kubernetes/Chart.yaml create mode 100644 kubernetes/README.md create mode 100644 kubernetes/templates/_helpers.tpl create mode 100644 kubernetes/templates/configmap.yaml create mode 100644 kubernetes/templates/deployment.yaml create mode 100644 kubernetes/templates/hpa.yaml create mode 100644 kubernetes/templates/service.yaml create mode 100644 kubernetes/values.yaml diff --git a/kubernetes/.gitignore b/kubernetes/.gitignore new file mode 100644 index 00000000..0ad51707 --- /dev/null +++ b/kubernetes/.gitignore @@ -0,0 +1 @@ +/charts/*.tgz diff --git a/kubernetes/Chart.lock b/kubernetes/Chart.lock new file mode 100644 index 00000000..1799798b --- /dev/null +++ b/kubernetes/Chart.lock @@ -0,0 +1,6 @@ +dependencies: +- name: postgresql + repository: https://kubernetes-charts.storage.googleapis.com/ + version: 8.3.0 +digest: sha256:1feec3c396cbf27573dc201831ccd3376a4a6b58b2e7618ce30a89b8f5d707fd +generated: "2020-02-07T13:39:38.624846+01:00" diff --git a/kubernetes/Chart.yaml b/kubernetes/Chart.yaml new file mode 100644 index 00000000..0d7791d7 --- /dev/null +++ b/kubernetes/Chart.yaml @@ -0,0 +1,22 @@ +apiVersion: v2 +name: invidious +description: Invidious is an alternative front-end to YouTube +version: 1.0.0 +appVersion: 0.20.1 +keywords: +- youtube +- proxy +- video +- privacy +home: https://invidio.us/ +icon: https://raw.githubusercontent.com/omarroth/invidious/05988c1c49851b7d0094fca16aeaf6382a7f64ab/assets/favicon-32x32.png +sources: +- https://github.com/omarroth/invidious +maintainers: +- name: Leon Klingele + email: mail@leonklingele.de +dependencies: +- name: postgresql + version: ~8.3.0 + repository: "https://kubernetes-charts.storage.googleapis.com/" +engine: gotpl diff --git a/kubernetes/README.md b/kubernetes/README.md new file mode 100644 index 00000000..163e9cd7 --- /dev/null +++ b/kubernetes/README.md @@ -0,0 +1,42 @@ +# Invidious Helm chart + +Easily deploy Invidious to Kubernetes. + +## Installing Helm chart + +```sh +# Build Helm dependencies +$ helm dep build + +# Add PostgreSQL init scripts +$ kubectl create configmap invidious-postgresql-init \ + --from-file=../config/sql/channels.sql \ + --from-file=../config/sql/videos.sql \ + --from-file=../config/sql/channel_videos.sql \ + --from-file=../config/sql/users.sql \ + --from-file=../config/sql/session_ids.sql \ + --from-file=../config/sql/nonces.sql \ + --from-file=../config/sql/annotations.sql \ + --from-file=../config/sql/playlists.sql \ + --from-file=../config/sql/playlist_videos.sql \ + --from-file=../config/sql/privacy.sql + +# Install Helm app to your Kubernetes cluster +$ helm install invidious ./ +``` + +## Upgrading + +```sh +# Upgrading is easy, too! +$ helm upgrade invidious ./ +``` + +## Uninstall + +```sh +# Get rid of everything (except database) +$ helm delete invidious + +# To also delete the database, remove all invidious-postgresql PVCs +``` diff --git a/kubernetes/templates/_helpers.tpl b/kubernetes/templates/_helpers.tpl new file mode 100644 index 00000000..52158b78 --- /dev/null +++ b/kubernetes/templates/_helpers.tpl @@ -0,0 +1,16 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "invidious.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +*/}} +{{- define "invidious.fullname" -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} diff --git a/kubernetes/templates/configmap.yaml b/kubernetes/templates/configmap.yaml new file mode 100644 index 00000000..58542a31 --- /dev/null +++ b/kubernetes/templates/configmap.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ template "invidious.fullname" . }} + labels: + app: {{ template "invidious.name" . }} + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + release: {{ .Release.Name }} +data: + INVIDIOUS_CONFIG: | +{{ toYaml .Values.config | indent 4 }} diff --git a/kubernetes/templates/deployment.yaml b/kubernetes/templates/deployment.yaml new file mode 100644 index 00000000..34156127 --- /dev/null +++ b/kubernetes/templates/deployment.yaml @@ -0,0 +1,53 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ template "invidious.fullname" . }} + labels: + app: {{ template "invidious.name" . }} + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + release: {{ .Release.Name }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app: {{ template "invidious.name" . }} + release: {{ .Release.Name }} + template: + metadata: + labels: + app: {{ template "invidious.name" . }} + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + release: {{ .Release.Name }} + spec: + securityContext: + runAsUser: {{ .Values.securityContext.runAsUser }} + runAsGroup: {{ .Values.securityContext.runAsGroup }} + fsGroup: {{ .Values.securityContext.fsGroup }} + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - containerPort: 3000 + env: + - name: INVIDIOUS_CONFIG + valueFrom: + configMapKeyRef: + key: INVIDIOUS_CONFIG + name: {{ template "invidious.fullname" . }} + securityContext: + allowPrivilegeEscalation: {{ .Values.securityContext.allowPrivilegeEscalation }} + capabilities: + drop: + - ALL + resources: +{{ toYaml .Values.resources | indent 10 }} + readinessProbe: + httpGet: + port: 3000 + path: / + livenessProbe: + httpGet: + port: 3000 + path: / + restartPolicy: Always diff --git a/kubernetes/templates/hpa.yaml b/kubernetes/templates/hpa.yaml new file mode 100644 index 00000000..c6fbefe2 --- /dev/null +++ b/kubernetes/templates/hpa.yaml @@ -0,0 +1,18 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ template "invidious.fullname" . }} + labels: + app: {{ template "invidious.name" . }} + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + release: {{ .Release.Name }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ template "invidious.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + targetCPUUtilizationPercentage: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} +{{- end }} diff --git a/kubernetes/templates/service.yaml b/kubernetes/templates/service.yaml new file mode 100644 index 00000000..56bdea2e --- /dev/null +++ b/kubernetes/templates/service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ template "invidious.fullname" . }} + labels: + app: {{ template "invidious.name" . }} + chart: {{ .Chart.Name }} + release: {{ .Release.Name }} +spec: + ports: + - name: http + port: 3000 + targetPort: 3000 + selector: + app: {{ template "invidious.name" . }} + release: {{ .Release.Name }} diff --git a/kubernetes/values.yaml b/kubernetes/values.yaml new file mode 100644 index 00000000..ce32b257 --- /dev/null +++ b/kubernetes/values.yaml @@ -0,0 +1,51 @@ +name: invidious + +image: + repository: omarroth/invidious + tag: latest + pullPolicy: Always + +replicaCount: 1 + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 16 + targetCPUUtilizationPercentage: 50 + +resources: {} + #requests: + # cpu: 100m + # memory: 64Mi + #limits: + # cpu: 800m + # memory: 512Mi + +securityContext: + allowPrivilegeEscalation: false + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + +# See https://github.com/helm/charts/tree/master/stable/postgresql +postgresql: + postgresqlUsername: kemal + postgresqlPassword: kemal + postgresqlDatabase: invidious + initdbUsername: kemal + initdbPassword: kemal + initdbScriptsConfigMap: invidious-postgresql-init + +# Adapted from ../config/config.yml +config: + channel_threads: 1 + feed_threads: 1 + db: + user: kemal + password: kemal + host: invidious-postgresql + port: 5432 + dbname: invidious + full_refresh: false + https_only: false + domain: From bc9dc3bf1e7d2cb5b5289e6d1ef0443831bf2184 Mon Sep 17 00:00:00 2001 From: Leon Klingele Date: Sat, 15 Feb 2020 19:52:28 +0100 Subject: [PATCH 011/165] Update code formatting for Crystal 0.33.0 Crystal 0.33.0 introduced some changes to to the code formatter. Run "crystal tool format" so CI doesn't fail anymore. --- src/invidious.cr | 2 +- src/invidious/helpers/utils.cr | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index a4584d4b..d2ff2a3d 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -60,7 +60,7 @@ REQUEST_HEADERS_WHITELIST = {"accept", "accept-encoding", "cache-control", "con RESPONSE_HEADERS_BLACKLIST = {"access-control-allow-origin", "alt-svc", "server"} HTTP_CHUNK_SIZE = 10485760 # ~10MB -CURRENT_BRANCH = {{ "#{`git branch | sed -n '/\* /s///p'`.strip}" }} +CURRENT_BRANCH = {{ "#{`git branch | sed -n '/* /s///p'`.strip}" }} CURRENT_COMMIT = {{ "#{`git rev-list HEAD --max-count=1 --abbrev-commit`.strip}" }} CURRENT_VERSION = {{ "#{`git describe --tags --abbrev=0`.strip}" }} diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index 6fcfa8d2..e43ae71d 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -315,7 +315,7 @@ def get_referer(env, fallback = "/", unroll = true) end referer = referer.full_path - referer = "/" + referer.lstrip("\/\\") + referer = "/" + referer.lstrip("/\\") if referer == env.request.path referer = fallback From 0fb41b10e9ddeed10bde6ff6c293ec8a8485a1a9 Mon Sep 17 00:00:00 2001 From: Leander Seidlitz Date: Sat, 15 Feb 2020 20:57:50 +0100 Subject: [PATCH 012/165] readme.md: fix missing playlist relation in postgresql --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 8b006334..6f6e33dc 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,9 @@ $ 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 ``` From fea6b67067356b43655a0e66adf8cb78b4a9ec8b Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Thu, 6 Feb 2020 20:16:43 -0500 Subject: [PATCH 013/165] Remove 'type' attribute from community embed --- src/invidious/comments.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr index 9e4d3866..2938247a 100644 --- a/src/invidious/comments.cr +++ b/src/invidious/comments.cr @@ -347,7 +347,7 @@ def template_youtube_comments(comments, locale, thin_mode) END_HTML else html << <<-END_HTML - + END_HTML end From 43da06a354658100f1d2cb0993ce744ac0e61727 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Thu, 20 Feb 2020 18:30:35 -0500 Subject: [PATCH 014/165] Remove temp fix for crystal/crystal-lang#7383 --- src/invidious/helpers/handlers.cr | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/src/invidious/helpers/handlers.cr b/src/invidious/helpers/handlers.cr index 456618cf..87b10bc9 100644 --- a/src/invidious/helpers/handlers.cr +++ b/src/invidious/helpers/handlers.cr @@ -212,29 +212,3 @@ class DenyFrame < Kemal::Handler call_next env end end - -# Temp fixes for https://github.com/crystal-lang/crystal/issues/7383 -class HTTP::UnknownLengthContent - def read_byte - ensure_send_continue - if @io.is_a?(OpenSSL::SSL::Socket::Client) - return if @io.as(OpenSSL::SSL::Socket::Client).@in_buffer_rem.empty? - end - @io.read_byte - end -end - -class HTTP::Client - private def handle_response(response) - if @socket.is_a?(OpenSSL::SSL::Socket::Client) && @host.ends_with?("googlevideo.com") - close unless response.keep_alive? || @socket.as(OpenSSL::SSL::Socket::Client).@in_buffer_rem.empty? - - if @socket.as(OpenSSL::SSL::Socket::Client).@in_buffer_rem.empty? - @socket = nil - end - else - close unless response.keep_alive? - end - response - end -end From 239fb0db9447590482fddb3b2e638e882b97847a Mon Sep 17 00:00:00 2001 From: Pedro Lucas Porcellis Date: Thu, 20 Feb 2020 20:50:54 -0300 Subject: [PATCH 015/165] Remove duplicated Github logo on footer (#986) * Remove duplicated Github logo on footer --- src/invidious/views/template.ecr | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr index b7cf2dcb..d2ef9c7e 100644 --- a/src/invidious/views/template.ecr +++ b/src/invidious/views/template.ecr @@ -140,9 +140,7 @@
- <%= 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 016/165] 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 017/165] 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 018/165] 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 019/165] 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 020/165] 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 021/165] 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 022/165] 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 023/165] 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 024/165] 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 025/165] 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 026/165] 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 027/165] 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 028/165] 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 029/165] 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 030/165] 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 031/165] 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 032/165] 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 033/165] 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 034/165] 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 035/165] 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 036/165] 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 037/165] 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 038/165] 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 039/165] 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 040/165] 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 041/165] 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 042/165] 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 043/165] 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 044/165] 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 045/165] 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 046/165] 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 047/165] 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 048/165] 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 049/165] 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 050/165] 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 051/165] 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 052/165] 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 053/165] 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 054/165] 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 055/165] 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 056/165] 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 057/165] 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 058/165] 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 059/165] 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 060/165] 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 061/165] 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 062/165] 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 063/165] 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 064/165] 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 065/165] 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 066/165] 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 067/165] 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 068/165] 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 069/165] 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 070/165] 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 071/165] 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 072/165] 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 073/165] 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 074/165] 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 075/165] 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 076/165] 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 077/165] 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 078/165] 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 079/165] 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 080/165] 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 081/165] 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 082/165] 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 083/165] 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 084/165] 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 085/165] 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 086/165] 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 087/165] 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 088/165] 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 089/165] 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 090/165] 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 @@