From 07de1e236f9225ccbebbccedb76036a340837430 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sun, 22 Oct 2023 17:09:52 +0200 Subject: Videos: Append '&mpd_version=5' to DASH manifest URL This makes Youtube return a MPD manifest with templates rather than lengthy . The returned manifest is about 44 times smaller. --- src/invidious/videos.cr | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index 9fbd1374..a8f02056 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -227,8 +227,22 @@ struct Video info.dig?("streamingData", "hlsManifestUrl").try &.as_s end - def dash_manifest_url - info.dig?("streamingData", "dashManifestUrl").try &.as_s + def dash_manifest_url : String? + raw_dash_url = info.dig?("streamingData", "dashManifestUrl").try &.as_s + return nil if raw_dash_url.nil? + + # Use manifest v5 parameter to reduce file size + # See https://github.com/iv-org/invidious/issues/4186 + dash_url = URI.parse(raw_dash_url) + dash_query = dash_url.query || "" + + if dash_query.empty? + dash_url.path = "#{dash_url.path}/mpd_version/5" + else + dash_url.query = "#{dash_query}&mpd_version=5" + end + + return dash_url.to_s end def genre_url : String? -- cgit v1.2.3 From 85a5bbd696da793e32556c50b8ae946b9358fe17 Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Thu, 26 Oct 2023 17:24:53 -0400 Subject: Add playlist and start time to the resolve url --- src/invidious/routes/api/v1/misc.cr | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src') diff --git a/src/invidious/routes/api/v1/misc.cr b/src/invidious/routes/api/v1/misc.cr index 8a92e160..c23fb0a5 100644 --- a/src/invidious/routes/api/v1/misc.cr +++ b/src/invidious/routes/api/v1/misc.cr @@ -175,6 +175,8 @@ module Invidious::Routes::API::V1::Misc json.object do json.field "ucid", sub_endpoint["browseId"].as_s if sub_endpoint["browseId"]? json.field "videoId", sub_endpoint["videoId"].as_s if sub_endpoint["videoId"]? + json.field "playlistId", sub_endpoint["playlistId"].as_s if sub_endpoint["playlistId"]? + json.field "startTimeSeconds", sub_endpoint["startTimeSeconds"].as_s if sub_endpoint["startTimeSeconds"]? json.field "params", params.try &.as_s json.field "pageType", pageType end -- cgit v1.2.3 From d7901c1e0d1e9b9d36cc35d35f43c91b88ebcce4 Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Thu, 26 Oct 2023 17:35:52 -0400 Subject: type fix --- src/invidious/routes/api/v1/misc.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/invidious/routes/api/v1/misc.cr b/src/invidious/routes/api/v1/misc.cr index c23fb0a5..bad47709 100644 --- a/src/invidious/routes/api/v1/misc.cr +++ b/src/invidious/routes/api/v1/misc.cr @@ -176,7 +176,7 @@ module Invidious::Routes::API::V1::Misc json.field "ucid", sub_endpoint["browseId"].as_s if sub_endpoint["browseId"]? json.field "videoId", sub_endpoint["videoId"].as_s if sub_endpoint["videoId"]? json.field "playlistId", sub_endpoint["playlistId"].as_s if sub_endpoint["playlistId"]? - json.field "startTimeSeconds", sub_endpoint["startTimeSeconds"].as_s if sub_endpoint["startTimeSeconds"]? + json.field "startTimeSeconds", sub_endpoint["startTimeSeconds"].as_i if sub_endpoint["startTimeSeconds"]? json.field "params", params.try &.as_s json.field "pageType", pageType end -- cgit v1.2.3 From 7e267da5beef5981b6db40e7b20f23f5dbd81136 Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Thu, 26 Oct 2023 17:48:58 -0400 Subject: Make head request to resolve short urls --- src/invidious/routes/api/v1/misc.cr | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'src') diff --git a/src/invidious/routes/api/v1/misc.cr b/src/invidious/routes/api/v1/misc.cr index bad47709..47ec977e 100644 --- a/src/invidious/routes/api/v1/misc.cr +++ b/src/invidious/routes/api/v1/misc.cr @@ -159,6 +159,11 @@ module Invidious::Routes::API::V1::Misc return error_json(400, "Missing URL to resolve") if !url begin + head_response = HTTP::Client.head url.as(String) + if head_response.headers["location"]? + url = head_response.headers["location"] + end + resolved_url = YoutubeAPI.resolve_url(url.as(String)) endpoint = resolved_url["endpoint"] pageType = endpoint.dig?("commandMetadata", "webCommandMetadata", "webPageType").try &.as_s || "" -- cgit v1.2.3 From 3881038a32cde54bb31523adb4bbc8fd5b3d759a Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Thu, 26 Oct 2023 17:51:38 -0400 Subject: format --- src/invidious/routes/api/v1/misc.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/invidious/routes/api/v1/misc.cr b/src/invidious/routes/api/v1/misc.cr index 47ec977e..c6662e32 100644 --- a/src/invidious/routes/api/v1/misc.cr +++ b/src/invidious/routes/api/v1/misc.cr @@ -163,7 +163,7 @@ module Invidious::Routes::API::V1::Misc if head_response.headers["location"]? url = head_response.headers["location"] end - + resolved_url = YoutubeAPI.resolve_url(url.as(String)) endpoint = resolved_url["endpoint"] pageType = endpoint.dig?("commandMetadata", "webCommandMetadata", "webPageType").try &.as_s || "" -- cgit v1.2.3 From db3c57d49f35ee03a634ddd41dc0c0f19dfb4f5f Mon Sep 17 00:00:00 2001 From: "Émilien (perso)" <4016501+unixfox@users.noreply.github.com> Date: Thu, 2 Nov 2023 23:35:50 +0100 Subject: force resolve for everything (#4225) --- src/invidious/yt_backend/connection_pool.cr | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/invidious/yt_backend/connection_pool.cr b/src/invidious/yt_backend/connection_pool.cr index e9eb726c..03ff0ee4 100644 --- a/src/invidious/yt_backend/connection_pool.cr +++ b/src/invidious/yt_backend/connection_pool.cr @@ -37,7 +37,7 @@ struct YoutubeConnectionPool conn.close conn = HTTP::Client.new(url) - conn.family = (url.host == "www.youtube.com") ? CONFIG.force_resolve : Socket::Family::INET + conn.family = CONFIG.force_resolve conn.family = Socket::Family::INET if conn.family == Socket::Family::UNSPEC conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com" response = yield conn @@ -52,7 +52,7 @@ struct YoutubeConnectionPool private def build_pool DB::Pool(HTTP::Client).new(initial_pool_size: 0, max_pool_size: capacity, max_idle_pool_size: capacity, checkout_timeout: timeout) do conn = HTTP::Client.new(url) - conn.family = (url.host == "www.youtube.com") ? CONFIG.force_resolve : Socket::Family::INET + conn.family = CONFIG.force_resolve conn.family = Socket::Family::INET if conn.family == Socket::Family::UNSPEC conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com" conn @@ -62,7 +62,7 @@ end def make_client(url : URI, region = nil) client = HTTPClient.new(url, OpenSSL::SSL::Context::Client.insecure) - client.family = (url.host == "www.youtube.com") ? CONFIG.force_resolve : Socket::Family::UNSPEC + client.family = CONFIG.force_resolve client.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com" client.read_timeout = 10.seconds client.connect_timeout = 10.seconds -- cgit v1.2.3 From 438467f69a20007cace4f300d03138b7d2d7e79a Mon Sep 17 00:00:00 2001 From: syeopite <70992037+syeopite@users.noreply.github.com> Date: Sat, 4 Nov 2023 13:52:30 +0000 Subject: Add playback success rate to `/api/v1/stats` (#4085) * Add stats-based /videoplayback blockage status * Count when YouTube returns wrong video as failure * Cast playback stats hash type prior to return * Bump stats refresh timer to 10 minutes --- src/invidious/helpers/helpers.cr | 17 +++++++++++++++++ src/invidious/jobs/statistics_refresh_job.cr | 12 +++++++++++- src/invidious/routes/api/v1/misc.cr | 16 ++++++++++++++++ src/invidious/routes/video_playback.cr | 5 +++++ src/invidious/videos/parser.cr | 5 +++++ 5 files changed, 54 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr index 23ff0da9..6dc9860e 100644 --- a/src/invidious/helpers/helpers.cr +++ b/src/invidious/helpers/helpers.cr @@ -208,3 +208,20 @@ def proxy_file(response, env) IO.copy response.body_io, env.response end end + +# Fetch the playback requests tracker from the statistics endpoint. +# +# Creates a new tracker when unavailable. +def get_playback_statistic + if (tracker = Invidious::Jobs::StatisticsRefreshJob::STATISTICS["playback"]) && tracker.as(Hash).empty? + tracker = { + "totalRequests" => 0_i64, + "successfulRequests" => 0_i64, + "ratio" => 0_f64, + } + + Invidious::Jobs::StatisticsRefreshJob::STATISTICS["playback"] = tracker + end + + return tracker.as(Hash(String, Int64 | Float64)) +end diff --git a/src/invidious/jobs/statistics_refresh_job.cr b/src/invidious/jobs/statistics_refresh_job.cr index a113bd77..72d1ce88 100644 --- a/src/invidious/jobs/statistics_refresh_job.cr +++ b/src/invidious/jobs/statistics_refresh_job.cr @@ -18,6 +18,13 @@ class Invidious::Jobs::StatisticsRefreshJob < Invidious::Jobs::BaseJob "updatedAt" => Time.utc.to_unix, "lastChannelRefreshedAt" => 0_i64, }, + + # + # "totalRequests" => 0_i64, + # "successfulRequests" => 0_i64 + # "ratio" => 0_i64 + # + "playback" => {} of String => Int64 | Float64, } private getter db : DB::Database @@ -30,7 +37,7 @@ class Invidious::Jobs::StatisticsRefreshJob < Invidious::Jobs::BaseJob loop do refresh_stats - sleep 1.minute + sleep 10.minute Fiber.yield end end @@ -56,5 +63,8 @@ class Invidious::Jobs::StatisticsRefreshJob < Invidious::Jobs::BaseJob "updatedAt" => Time.utc.to_unix, "lastChannelRefreshedAt" => Invidious::Database::Statistics.channel_last_update.try &.to_unix || 0_i64, } + + # Reset playback requests tracker + STATISTICS["playback"] = {} of String => Int64 | Float64 end end diff --git a/src/invidious/routes/api/v1/misc.cr b/src/invidious/routes/api/v1/misc.cr index 8a92e160..b42ecd1a 100644 --- a/src/invidious/routes/api/v1/misc.cr +++ b/src/invidious/routes/api/v1/misc.cr @@ -6,6 +6,22 @@ module Invidious::Routes::API::V1::Misc if !CONFIG.statistics_enabled return {"software" => SOFTWARE}.to_json else + # Calculate playback success rate + if (tracker = Invidious::Jobs::StatisticsRefreshJob::STATISTICS["playback"]?) + tracker = tracker.as(Hash(String, Int64 | Float64)) + + if !tracker.empty? + total_requests = tracker["totalRequests"] + success_count = tracker["successfulRequests"] + + if total_requests.zero? + tracker["ratio"] = 1_i64 + else + tracker["ratio"] = (success_count / (total_requests)).round(2) + end + end + end + return Invidious::Jobs::StatisticsRefreshJob::STATISTICS.to_json end end diff --git a/src/invidious/routes/video_playback.cr b/src/invidious/routes/video_playback.cr index 9641e01a..1d5aa914 100644 --- a/src/invidious/routes/video_playback.cr +++ b/src/invidious/routes/video_playback.cr @@ -80,9 +80,14 @@ module Invidious::Routes::VideoPlayback # Remove the Range header added previously. headers.delete("Range") if range_header.nil? + playback_statistics = get_playback_statistic() + playback_statistics["totalRequests"] += 1 + if response.status_code >= 400 env.response.content_type = "text/plain" haltf env, response.status_code + else + playback_statistics["successfulRequests"] += 1 end if url.includes? "&file=seg.ts" diff --git a/src/invidious/videos/parser.cr b/src/invidious/videos/parser.cr index 551ce2cb..77520dbe 100644 --- a/src/invidious/videos/parser.cr +++ b/src/invidious/videos/parser.cr @@ -78,6 +78,11 @@ def extract_video_info(video_id : String, proxy_region : String? = nil) # YouTube may return a different video player response than expected. # See: https://github.com/TeamNewPipe/NewPipe/issues/8713 # Line to be reverted if one day we solve the video not available issue. + + # Although technically not a call to /videoplayback the fact that YouTube is returning the + # wrong video means that we should count it as a failure. + get_playback_statistic()["totalRequests"] += 1 + return { "version" => JSON::Any.new(Video::SCHEMA_VERSION.to_i64), "reason" => JSON::Any.new("Can't load the video on this Invidious instance. YouTube is currently trying to block Invidious instances. Click here for more info about the issue."), -- cgit v1.2.3 From c31908a011f5ac460de7c816f8861a6ec3cb6692 Mon Sep 17 00:00:00 2001 From: syeopite Date: Tue, 7 Nov 2023 22:19:56 -0800 Subject: Update params protobuf for channel about tab --- src/invidious/channels/about.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/invidious/channels/about.cr b/src/invidious/channels/about.cr index 0054f8f2..894670dd 100644 --- a/src/invidious/channels/about.cr +++ b/src/invidious/channels/about.cr @@ -18,8 +18,8 @@ record AboutChannel, def get_about_info(ucid, locale) : AboutChannel begin - # "EgVhYm91dA==" is the base64-encoded protobuf object {"2:string":"about"} - initdata = YoutubeAPI.browse(browse_id: ucid, params: "EgVhYm91dA==") + # "EgVhYm91dPIGBAoCEgA=" is the base64-encoded protobuf object {"2:0:string":"about","110:1:embedded":{"1:0:embedded":{"2:0:string":""}}} + initdata = YoutubeAPI.browse(browse_id: ucid, params: "EgVhYm91dPIGBAoCEgA=") rescue raise InfoException.new("Could not get channel info.") end -- cgit v1.2.3 From ab4c0a1d3c5af5c1ba50364c04e1a996dbabc152 Mon Sep 17 00:00:00 2001 From: syeopite Date: Tue, 7 Nov 2023 23:45:44 -0800 Subject: Fetch channel info from home tab instead of about --- src/invidious/channels/about.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/invidious/channels/about.cr b/src/invidious/channels/about.cr index 894670dd..8b60a728 100644 --- a/src/invidious/channels/about.cr +++ b/src/invidious/channels/about.cr @@ -18,8 +18,8 @@ record AboutChannel, def get_about_info(ucid, locale) : AboutChannel begin - # "EgVhYm91dPIGBAoCEgA=" is the base64-encoded protobuf object {"2:0:string":"about","110:1:embedded":{"1:0:embedded":{"2:0:string":""}}} - initdata = YoutubeAPI.browse(browse_id: ucid, params: "EgVhYm91dPIGBAoCEgA=") + # Fetch channel information from channel home page + initdata = YoutubeAPI.browse(browse_id: ucid, params: "") rescue raise InfoException.new("Could not get channel info.") end -- cgit v1.2.3 From 85257585836b7062e3d6d0ddd29fb3d5f6570b49 Mon Sep 17 00:00:00 2001 From: syeopite Date: Wed, 8 Nov 2023 00:19:24 -0800 Subject: Use #splat method for macro expressions --- src/invidious/helpers/errors.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/invidious/helpers/errors.cr b/src/invidious/helpers/errors.cr index 6e5a975d..a94a1991 100644 --- a/src/invidious/helpers/errors.cr +++ b/src/invidious/helpers/errors.cr @@ -3,7 +3,7 @@ # ------------------- macro error_template(*args) - error_template_helper(env, {{*args}}) + error_template_helper(env, {{args.splat}}) end def github_details(summary : String, content : String) -- cgit v1.2.3 From 8ce91166d66ff93a0ef791fcc7f53f7ae94f8271 Mon Sep 17 00:00:00 2001 From: syeopite Date: Wed, 8 Nov 2023 00:42:46 -0800 Subject: Remove instance of the * operator in macro expr --- src/invidious/helpers/errors.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/invidious/helpers/errors.cr b/src/invidious/helpers/errors.cr index a94a1991..70850063 100644 --- a/src/invidious/helpers/errors.cr +++ b/src/invidious/helpers/errors.cr @@ -121,7 +121,7 @@ end # ------------------- macro error_json(*args) - error_json_helper(env, {{*args}}) + error_json_helper(env, {{args.splat}}) end def error_json_helper( -- cgit v1.2.3 From ed8b84ed15616cd269ff5759fcc1fa5150259db8 Mon Sep 17 00:00:00 2001 From: syeopite Date: Wed, 8 Nov 2023 00:49:37 -0800 Subject: Replace more * in macro with #splat --- src/invidious/channels/channels.cr | 2 +- src/invidious/config.cr | 2 +- src/invidious/helpers/errors.cr | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/invidious/channels/channels.cr b/src/invidious/channels/channels.cr index c3d6124f..be739673 100644 --- a/src/invidious/channels/channels.cr +++ b/src/invidious/channels/channels.cr @@ -93,7 +93,7 @@ struct ChannelVideo def to_tuple {% begin %} { - {{*@type.instance_vars.map(&.name)}} + {{@type.instance_vars.map(&.name).splat}} } {% end %} end diff --git a/src/invidious/config.cr b/src/invidious/config.cr index 429d9246..d5d5f2f2 100644 --- a/src/invidious/config.cr +++ b/src/invidious/config.cr @@ -48,7 +48,7 @@ struct ConfigPreferences def to_tuple {% begin %} { - {{*@type.instance_vars.map { |var| "#{var.name}: #{var.name}".id }}} + {{(@type.instance_vars.map { |var| "#{var.name}: #{var.name}".id }).splat}} } {% end %} end diff --git a/src/invidious/helpers/errors.cr b/src/invidious/helpers/errors.cr index 70850063..21b789bc 100644 --- a/src/invidious/helpers/errors.cr +++ b/src/invidious/helpers/errors.cr @@ -95,7 +95,7 @@ end # ------------------- macro error_atom(*args) - error_atom_helper(env, {{*args}}) + error_atom_helper(env, {{args.splat}}) end def error_atom_helper(env : HTTP::Server::Context, status_code : Int32, exception : Exception) -- cgit v1.2.3 From 86ee7617887469739fda1dc242442fea93af3187 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Wed, 15 Nov 2023 00:51:43 -0500 Subject: Fix logic for setting user agent --- src/invidious/yt_backend/connection_pool.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/invidious/yt_backend/connection_pool.cr b/src/invidious/yt_backend/connection_pool.cr index 03ff0ee4..fbf05803 100644 --- a/src/invidious/yt_backend/connection_pool.cr +++ b/src/invidious/yt_backend/connection_pool.cr @@ -1,6 +1,6 @@ def add_yt_headers(request) if request.headers["User-Agent"] == "Crystal" - request.headers["User-Agent"] ||= "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" + request.headers["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" end request.headers["Accept-Charset"] ||= "ISO-8859-1,utf-8;q=0.7,*;q=0.7" -- cgit v1.2.3 From 8338a73e7bc0585edd36bb7fcd4589e741ce0bab Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Fri, 17 Nov 2023 07:48:34 -0500 Subject: add user_agent if empty or crystal --- src/invidious/yt_backend/connection_pool.cr | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/invidious/yt_backend/connection_pool.cr b/src/invidious/yt_backend/connection_pool.cr index fbf05803..36e82766 100644 --- a/src/invidious/yt_backend/connection_pool.cr +++ b/src/invidious/yt_backend/connection_pool.cr @@ -1,7 +1,6 @@ def add_yt_headers(request) - if request.headers["User-Agent"] == "Crystal" - request.headers["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" - end + request.headers.delete("User-Agent") if request.headers["User-Agent"] == "Crystal" + request.headers["User-Agent"] ||= "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 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" -- cgit v1.2.3 From b40cf6544a5e801c387b988f1be4d632fd50db90 Mon Sep 17 00:00:00 2001 From: Brahim Hadriche Date: Sun, 19 Nov 2023 16:06:29 -0500 Subject: Revert "Make head request to resolve short urls" This reverts commit 7e267da5beef5981b6db40e7b20f23f5dbd81136. --- src/invidious/routes/api/v1/misc.cr | 5 ----- 1 file changed, 5 deletions(-) (limited to 'src') diff --git a/src/invidious/routes/api/v1/misc.cr b/src/invidious/routes/api/v1/misc.cr index c6662e32..bad47709 100644 --- a/src/invidious/routes/api/v1/misc.cr +++ b/src/invidious/routes/api/v1/misc.cr @@ -159,11 +159,6 @@ module Invidious::Routes::API::V1::Misc return error_json(400, "Missing URL to resolve") if !url begin - head_response = HTTP::Client.head url.as(String) - if head_response.headers["location"]? - url = head_response.headers["location"] - end - resolved_url = YoutubeAPI.resolve_url(url.as(String)) endpoint = resolved_url["endpoint"] pageType = endpoint.dig?("commandMetadata", "webCommandMetadata", "webPageType").try &.as_s || "" -- cgit v1.2.3 From d9416a0be587ba3ceaa39e0a37e70ed669f7a6ae Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Mon, 20 Nov 2023 17:39:13 +0100 Subject: Jobs: Remove BypassCaptchaJob --- src/invidious/jobs/bypass_captcha_job.cr | 135 ------------------------------- 1 file changed, 135 deletions(-) delete mode 100644 src/invidious/jobs/bypass_captcha_job.cr (limited to 'src') diff --git a/src/invidious/jobs/bypass_captcha_job.cr b/src/invidious/jobs/bypass_captcha_job.cr deleted file mode 100644 index 71f8a938..00000000 --- a/src/invidious/jobs/bypass_captcha_job.cr +++ /dev/null @@ -1,135 +0,0 @@ -class Invidious::Jobs::BypassCaptchaJob < Invidious::Jobs::BaseJob - def begin - loop do - begin - random_video = PG_DB.query_one?("select id, ucid from (select id, ucid from channel_videos limit 1000) as s ORDER BY RANDOM() LIMIT 1", as: {id: String, ucid: String}) - if !random_video - random_video = {id: "zj82_v2R6ts", ucid: "UCK87Lox575O_HCHBWaBSyGA"} - end - {"/watch?v=#{random_video["id"]}&gl=US&hl=en&has_verified=1&bpctr=9999999999", produce_channel_videos_url(ucid: random_video["ucid"])}.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[@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| - inputs[node["name"]] = node["value"] - end - - headers = response.cookies.add_request_headers(HTTP::Headers.new) - - response = JSON.parse(HTTP::Client.post(CONFIG.captcha_api_url + "/createTask", - headers: HTTP::Headers{"Content-Type" => "application/json"}, body: { - "clientKey" => CONFIG.captcha_key, - "task" => { - "type" => "NoCaptchaTaskProxyless", - "websiteURL" => "https://www.youtube.com#{path}", - "websiteKey" => site_key, - "recaptchaDataSValue" => s_value, - }, - }.to_json).body) - - raise response["error"].as_s if response["error"]? - task_id = response["taskId"].as_i - - loop do - sleep 10.seconds - - response = JSON.parse(HTTP::Client.post(CONFIG.captcha_api_url + "/getTaskResult", - headers: HTTP::Headers{"Content-Type" => "application/json"}, 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 - headers["Cookies"] = response["solution"]["cookies"].as_h?.try &.map { |k, v| "#{k}=#{v}" }.join("; ") || "" - response = YT_POOL.client &.post("/das_captcha", headers, form: inputs) - - response.cookies - .select { |cookie| cookie.name != "PREF" } - .each { |cookie| CONFIG.cookies << cookie } - - # Persist cookies between runs - File.write("config/config.yml", CONFIG.to_yaml) - elsif response.headers["Location"]?.try &.includes?("/sorry/index") - location = response.headers["Location"].try { |u| URI.parse(u) } - headers = HTTP::Headers{":authority" => location.host.not_nil!} - response = YT_POOL.client &.get(location.request_target, headers) - - html = XML.parse_html(response.body) - form = html.xpath_node(%(//form[@action="index"])).not_nil! - 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| - inputs[node["name"]] = node["value"] - end - - captcha_client = HTTPClient.new(URI.parse(CONFIG.captcha_api_url)) - captcha_client.family = CONFIG.force_resolve || Socket::Family::INET - response = JSON.parse(captcha_client.post("/createTask", - headers: HTTP::Headers{"Content-Type" => "application/json"}, body: { - "clientKey" => CONFIG.captcha_key, - "task" => { - "type" => "NoCaptchaTaskProxyless", - "websiteURL" => location.to_s, - "websiteKey" => site_key, - "recaptchaDataSValue" => s_value, - }, - }.to_json).body) - - captcha_client.close - - raise response["error"].as_s if response["error"]? - task_id = response["taskId"].as_i - - loop do - sleep 10.seconds - - response = JSON.parse(captcha_client.post("/getTaskResult", - headers: HTTP::Headers{"Content-Type" => "application/json"}, 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 - headers["Cookies"] = response["solution"]["cookies"].as_h?.try &.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], - } - cookies = HTTP::Cookies.from_client_headers(headers) - - cookies.each { |cookie| CONFIG.cookies << cookie } - - # Persist cookies between runs - File.write("config/config.yml", CONFIG.to_yaml) - end - end - rescue ex - LOGGER.error("BypassCaptchaJob: #{ex.message}") - ensure - sleep 1.minute - Fiber.yield - end - end - end -end -- cgit v1.2.3 From 7e363fa3c854c0cc9b7f7815bd1da973cb86e2e1 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Mon, 20 Nov 2023 17:39:51 +0100 Subject: Config: Remove anti-captcha related configs --- config/config.example.yml | 21 --------------------- src/invidious/config.cr | 4 ---- 2 files changed, 25 deletions(-) (limited to 'src') diff --git a/config/config.example.yml b/config/config.example.yml index b44fcc0e..38085a20 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -392,27 +392,6 @@ jobs: enable: true -# ----------------------------- -# Captcha API -# ----------------------------- - -## -## URL of the captcha solving service. -## -## Accepted values: any URL -## Default: https://api.anti-captcha.com -## -#captcha_api_url: https://api.anti-captcha.com - -## -## API key for the captcha solving service. -## -## Accepted values: a string -## Default: -## -#captcha_key: - - # ----------------------------- # Miscellaneous # ----------------------------- diff --git a/src/invidious/config.cr b/src/invidious/config.cr index 429d9246..13e53222 100644 --- a/src/invidious/config.cr +++ b/src/invidious/config.cr @@ -133,10 +133,6 @@ class Config # Saved cookies in "name1=value1; name2=value2..." format @[YAML::Field(converter: Preferences::StringToCookies)] property cookies : HTTP::Cookies = HTTP::Cookies.new - # Key for Anti-Captcha - property captcha_key : String? = nil - # API URL for Anti-Captcha - property captcha_api_url : String = "https://api.anti-captcha.com" # Playlist length limit property playlist_length_limit : Int32 = 500 -- cgit v1.2.3 From 3a5d40860239829de6575644235f01d6af35a4b0 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Mon, 20 Nov 2023 17:40:31 +0100 Subject: Remove leftover functions/specs used by the anti-captcha job --- spec/invidious/helpers_spec.cr | 12 ------------ src/invidious/channels/videos.cr | 6 ------ 2 files changed, 18 deletions(-) (limited to 'src') diff --git a/spec/invidious/helpers_spec.cr b/spec/invidious/helpers_spec.cr index 142e1653..9fbb6d6f 100644 --- a/spec/invidious/helpers_spec.cr +++ b/spec/invidious/helpers_spec.cr @@ -3,18 +3,6 @@ require "../spec_helper" CONFIG = Config.from_yaml(File.open("config/config.example.yml")) Spectator.describe "Helper" do - describe "#produce_channel_videos_url" do - it "correctly produces url for requesting page `x` of a channel's videos" do - # expect(produce_channel_videos_url(ucid: "UCXuqSBlHAE6Xw-yeJA0Tunw")).to eq("/browse_ajax?continuation=4qmFsgI8EhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaIEVnWjJhV1JsYjNNd0FqZ0JZQUZxQUxnQkFDQUFlZ0V4&gl=US&hl=en") - # - # expect(produce_channel_videos_url(ucid: "UCXuqSBlHAE6Xw-yeJA0Tunw", sort_by: "popular")).to eq("/browse_ajax?continuation=4qmFsgJAEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaJEVnWjJhV1JsYjNNd0FqZ0JZQUZxQUxnQkFDQUFlZ0V4R0FFPQ%3D%3D&gl=US&hl=en") - - # expect(produce_channel_videos_url(ucid: "UCXuqSBlHAE6Xw-yeJA0Tunw", page: 20)).to eq("/browse_ajax?continuation=4qmFsgJAEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaJEVnWjJhV1JsYjNNd0FqZ0JZQUZxQUxnQkFDQUFlZ0l5TUE9PQ%3D%3D&gl=US&hl=en") - - # expect(produce_channel_videos_url(ucid: "UC-9-kyTW8ZkZNDHQJ6FgpwQ", page: 20, sort_by: "popular")).to eq("/browse_ajax?continuation=4qmFsgJAEhhVQy05LWt5VFc4WmtaTkRIUUo2Rmdwd1EaJEVnWjJhV1JsYjNNd0FqZ0JZQUZxQUxnQkFDQUFlZ0l5TUJnQg%3D%3D&gl=US&hl=en") - end - end - describe "#produce_channel_search_continuation" do it "correctly produces token for searching a specific channel" do expect(produce_channel_search_continuation("UCXuqSBlHAE6Xw-yeJA0Tunw", "", 100)).to eq("4qmFsgJqEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaIEVnWnpaV0Z5WTJnd0FUZ0JZQUY2QkVkS2IxaTRBUUE9WgCaAilicm93c2UtZmVlZFVDWHVxU0JsSEFFNlh3LXllSkEwVHVud3NlYXJjaA%3D%3D") diff --git a/src/invidious/channels/videos.cr b/src/invidious/channels/videos.cr index beb86e08..351790d7 100644 --- a/src/invidious/channels/videos.cr +++ b/src/invidious/channels/videos.cr @@ -62,12 +62,6 @@ def produce_channel_videos_continuation(ucid, page = 1, auto_generated = nil, so return continuation end -# Used in bypass_captcha_job.cr -def produce_channel_videos_url(ucid, page = 1, auto_generated = nil, sort_by = "newest", v2 = false) - continuation = produce_channel_videos_continuation(ucid, page, auto_generated, sort_by, v2) - return "/browse_ajax?continuation=#{continuation}&gl=US&hl=en" -end - module Invidious::Channel::Tabs extend self -- cgit v1.2.3 From 9310d09f9371072a2f5dba94e9dbbc6d49efa0a3 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Mon, 20 Nov 2023 17:31:21 +0100 Subject: Kemal: remove APIHandler middleware --- src/invidious.cr | 1 - src/invidious/helpers/handlers.cr | 68 --------------------------------------- 2 files changed, 69 deletions(-) (limited to 'src') diff --git a/src/invidious.cr b/src/invidious.cr index e0bd0101..c8cac80e 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -217,7 +217,6 @@ public_folder "assets" Kemal.config.powered_by_header = false add_handler FilteredCompressHandler.new -add_handler APIHandler.new add_handler AuthHandler.new add_handler DenyFrame.new add_context_storage_type(Array(String)) diff --git a/src/invidious/helpers/handlers.cr b/src/invidious/helpers/handlers.cr index d140a858..cece289b 100644 --- a/src/invidious/helpers/handlers.cr +++ b/src/invidious/helpers/handlers.cr @@ -134,74 +134,6 @@ class AuthHandler < Kemal::Handler end end -class APIHandler < Kemal::Handler - {% for method in %w(GET POST PUT HEAD DELETE PATCH OPTIONS) %} - only ["/api/v1/*"], {{method}} - {% end %} - exclude ["/api/v1/auth/notifications"], "GET" - exclude ["/api/v1/auth/notifications"], "POST" - - def call(env) - return call_next env unless only_match? env - - env.response.headers["Access-Control-Allow-Origin"] = "*" - - # Since /api/v1/notifications is an event-stream, we don't want - # to wrap the response - return call_next env if exclude_match? env - - # Here we swap out the socket IO so we can modify the response as needed - output = env.response.output - env.response.output = IO::Memory.new - - begin - call_next env - - env.response.output.rewind - - if env.response.output.as(IO::Memory).size != 0 && - env.response.headers.includes_word?("Content-Type", "application/json") - response = JSON.parse(env.response.output) - - if fields_text = env.params.query["fields"]? - begin - JSONFilter.filter(response, fields_text) - rescue ex - env.response.status_code = 400 - response = {"error" => ex.message} - end - end - - if env.params.query["pretty"]?.try &.== "1" - response = response.to_pretty_json - else - response = response.to_json - end - else - response = env.response.output.gets_to_end - end - rescue ex - env.response.content_type = "application/json" if env.response.headers.includes_word?("Content-Type", "text/html") - env.response.status_code = 500 - - if env.response.headers.includes_word?("Content-Type", "application/json") - response = {"error" => ex.message || "Unspecified error"} - - if env.params.query["pretty"]?.try &.== "1" - response = response.to_pretty_json - else - response = response.to_json - end - end - ensure - env.response.output = output - env.response.print response - - env.response.flush - end - end -end - class DenyFrame < Kemal::Handler exclude ["/embed/*"] -- cgit v1.2.3 From 9d5fa2bcc4c708f15ce6e87e6da0c8922eece553 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Mon, 20 Nov 2023 17:33:26 +0100 Subject: Helpers: remove JSONFilter logic --- src/invidious/helpers/helpers.cr | 27 ---- src/invidious/helpers/json_filter.cr | 248 ----------------------------------- 2 files changed, 275 deletions(-) delete mode 100644 src/invidious/helpers/json_filter.cr (limited to 'src') diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr index 6dc9860e..6add0237 100644 --- a/src/invidious/helpers/helpers.cr +++ b/src/invidious/helpers/helpers.cr @@ -78,15 +78,6 @@ def create_notification_stream(env, topics, connection_channel) video.published = published response = JSON.parse(video.to_json(locale, nil)) - if fields_text = env.params.query["fields"]? - begin - JSONFilter.filter(response, fields_text) - rescue ex - env.response.status_code = 400 - response = {"error" => ex.message} - end - end - env.response.puts "id: #{id}" env.response.puts "data: #{response.to_json}" env.response.puts @@ -113,15 +104,6 @@ def create_notification_stream(env, topics, connection_channel) Invidious::Database::ChannelVideos.select_notfications(topic, since_unix).each do |video| response = JSON.parse(video.to_json(locale)) - if fields_text = env.params.query["fields"]? - begin - JSONFilter.filter(response, fields_text) - rescue ex - env.response.status_code = 400 - response = {"error" => ex.message} - end - end - env.response.puts "id: #{id}" env.response.puts "data: #{response.to_json}" env.response.puts @@ -155,15 +137,6 @@ def create_notification_stream(env, topics, connection_channel) video.published = Time.unix(published) response = JSON.parse(video.to_json(locale, nil)) - if fields_text = env.params.query["fields"]? - begin - JSONFilter.filter(response, fields_text) - rescue ex - env.response.status_code = 400 - response = {"error" => ex.message} - end - end - env.response.puts "id: #{id}" env.response.puts "data: #{response.to_json}" env.response.puts diff --git a/src/invidious/helpers/json_filter.cr b/src/invidious/helpers/json_filter.cr deleted file mode 100644 index 3f4080ba..00000000 --- a/src/invidious/helpers/json_filter.cr +++ /dev/null @@ -1,248 +0,0 @@ -module JSONFilter - alias BracketIndex = Hash(Int64, Int64) - - alias GroupedFieldsValue = String | Array(GroupedFieldsValue) - alias GroupedFieldsList = Array(GroupedFieldsValue) - - class FieldsParser - class ParseError < Exception - end - - # Returns the `Regex` pattern used to match nest groups - def self.nest_group_pattern : Regex - # uses a '.' character to match json keys as they are allowed - # to contain any unicode codepoint - /(?:|,)(?[^,\n]*?)\(/ - end - - # Returns the `Regex` pattern used to check if there are any empty nest groups - def self.unnamed_nest_group_pattern : Regex - /^\(|\(\(|\/\(/ - end - - def self.parse_fields(fields_text : String, &) : Nil - if fields_text.empty? - raise FieldsParser::ParseError.new "Fields is empty" - end - - opening_bracket_count = fields_text.count('(') - closing_bracket_count = fields_text.count(')') - - if opening_bracket_count != closing_bracket_count - bracket_type = opening_bracket_count > closing_bracket_count ? "opening" : "closing" - raise FieldsParser::ParseError.new "There are too many #{bracket_type} brackets (#{opening_bracket_count}:#{closing_bracket_count})" - elsif match_result = unnamed_nest_group_pattern.match(fields_text) - raise FieldsParser::ParseError.new "Unnamed nest group at position #{match_result.begin}" - end - - # first, handle top-level single nested properties: items/id, playlistItems/snippet, etc - parse_single_nests(fields_text) { |nest_list| yield nest_list } - - # next, handle nest groups: items(id, etag, etc) - parse_nest_groups(fields_text) { |nest_list| yield nest_list } - end - - def self.parse_single_nests(fields_text : String, &) : Nil - single_nests = remove_nest_groups(fields_text) - - if !single_nests.empty? - property_nests = single_nests.split(',') - - property_nests.each do |nest| - nest_list = nest.split('/') - if nest_list.includes? "" - raise FieldsParser::ParseError.new "Empty key in nest list: #{nest_list}" - end - yield nest_list - end - # else - # raise FieldsParser::ParseError.new "Empty key in nest list 22: #{fields_text} | #{single_nests}" - end - end - - def self.parse_nest_groups(fields_text : String, &) : Nil - nest_stack = [] of NamedTuple(group_name: String, closing_bracket_index: Int64) - bracket_pairs = get_bracket_pairs(fields_text, true) - - text_index = 0 - regex_index = 0 - - while regex_result = self.nest_group_pattern.match(fields_text, regex_index) - raw_match = regex_result[0] - group_name = regex_result["groupname"] - - text_index = regex_result.begin - regex_index = regex_result.end - - if text_index.nil? || regex_index.nil? - raise FieldsParser::ParseError.new "Received invalid index while parsing nest groups: text_index: #{text_index} | regex_index: #{regex_index}" - end - - offset = raw_match.starts_with?(',') ? 1 : 0 - - opening_bracket_index = (text_index + group_name.size) + offset - closing_bracket_index = bracket_pairs[opening_bracket_index] - content_start = opening_bracket_index + 1 - - content = fields_text[content_start...closing_bracket_index] - - if content.empty? - raise FieldsParser::ParseError.new "Empty nest group at position #{content_start}" - else - content = remove_nest_groups(content) - end - - while nest_stack.size > 0 && closing_bracket_index > nest_stack[nest_stack.size - 1][:closing_bracket_index] - if nest_stack.size - nest_stack.pop - end - end - - group_name.split('/').each do |name| - nest_stack.push({ - group_name: name, - closing_bracket_index: closing_bracket_index, - }) - end - - if !content.empty? - properties = content.split(',') - - properties.each do |prop| - nest_list = nest_stack.map { |nest_prop| nest_prop[:group_name] } - - if !prop.empty? - if prop.includes?('/') - parse_single_nests(prop) { |list| nest_list += list } - else - nest_list.push prop - end - else - raise FieldsParser::ParseError.new "Empty key in nest list: #{nest_list << prop}" - end - - yield nest_list - end - end - end - end - - def self.remove_nest_groups(text : String) : String - content_bracket_pairs = get_bracket_pairs(text, false) - - content_bracket_pairs.each_key.to_a.reverse.each do |opening_bracket| - closing_bracket = content_bracket_pairs[opening_bracket] - last_comma = text.rindex(',', opening_bracket) || 0 - - text = text[0...last_comma] + text[closing_bracket + 1...text.size] - end - - return text.starts_with?(',') ? text[1...text.size] : text - end - - def self.get_bracket_pairs(text : String, recursive = true) : BracketIndex - istart = [] of Int64 - bracket_index = BracketIndex.new - - text.each_char_with_index do |char, index| - if char == '(' - istart.push(index.to_i64) - end - - if char == ')' - begin - opening = istart.pop - if recursive || (!recursive && istart.size == 0) - bracket_index[opening] = index.to_i64 - end - rescue - raise FieldsParser::ParseError.new "No matching opening parenthesis at: #{index}" - end - end - end - - if istart.size != 0 - idx = istart.pop - raise FieldsParser::ParseError.new "No matching closing parenthesis at: #{idx}" - end - - return bracket_index - end - end - - class FieldsGrouper - alias SkeletonValue = Hash(String, SkeletonValue) - - def self.create_json_skeleton(fields_text : String) : SkeletonValue - root_hash = {} of String => SkeletonValue - - FieldsParser.parse_fields(fields_text) do |nest_list| - current_item = root_hash - nest_list.each do |key| - if current_item[key]? - current_item = current_item[key] - else - current_item[key] = {} of String => SkeletonValue - current_item = current_item[key] - end - end - end - root_hash - end - - def self.create_grouped_fields_list(json_skeleton : SkeletonValue) : GroupedFieldsList - grouped_fields_list = GroupedFieldsList.new - json_skeleton.each do |key, value| - grouped_fields_list.push key - - nested_keys = create_grouped_fields_list(value) - grouped_fields_list.push nested_keys unless nested_keys.empty? - end - return grouped_fields_list - end - end - - class FilterError < Exception - end - - def self.filter(item : JSON::Any, fields_text : String, in_place : Bool = true) - skeleton = FieldsGrouper.create_json_skeleton(fields_text) - grouped_fields_list = FieldsGrouper.create_grouped_fields_list(skeleton) - filter(item, grouped_fields_list, in_place) - end - - def self.filter(item : JSON::Any, grouped_fields_list : GroupedFieldsList, in_place : Bool = true) : JSON::Any - item = item.clone unless in_place - - if !item.as_h? && !item.as_a? - raise FilterError.new "Can't filter '#{item}' by #{grouped_fields_list}" - end - - top_level_keys = Array(String).new - grouped_fields_list.each do |value| - if value.is_a? String - top_level_keys.push value - elsif value.is_a? Array - if !top_level_keys.empty? - key_to_filter = top_level_keys.last - - if item.as_h? - filter(item[key_to_filter], value, in_place: true) - elsif item.as_a? - item.as_a.each { |arr_item| filter(arr_item[key_to_filter], value, in_place: true) } - end - else - raise FilterError.new "Tried to filter while top level keys list is empty" - end - end - end - - if item.as_h? - item.as_h.select! top_level_keys - elsif item.as_a? - item.as_a.map { |value| filter(value, top_level_keys, in_place: true) } - end - - item - end -end -- cgit v1.2.3 From 64887942184543c6cb5ada0909e6e1a594fdb863 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 31 Oct 2023 13:24:20 -0400 Subject: Unescape search suggestions --- src/invidious/routes/api/v1/search.cr | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/invidious/routes/api/v1/search.cr b/src/invidious/routes/api/v1/search.cr index 9fb283c2..a65571ea 100644 --- a/src/invidious/routes/api/v1/search.cr +++ b/src/invidious/routes/api/v1/search.cr @@ -32,11 +32,13 @@ module Invidious::Routes::API::V1::Search begin client = HTTP::Client.new("suggestqueries-clients6.youtube.com") - url = "/complete/search?client=youtube&hl=en&gl=#{region}&q=#{URI.encode_www_form(query)}&xssi=t&gs_ri=youtube&ds=yt" + client.before_request { |r| add_yt_headers(r) } + + url = "/complete/search?client=youtube&hl=en&gl=#{region}&q=#{URI.encode_www_form(query)}&gs_ri=youtube&ds=yt" response = client.get(url).body - body = JSON.parse(response[5..-1]).as_a + body = JSON.parse(response[19..-2]).as_a suggestions = body[1].as_a[0..-2] JSON.build do |json| -- cgit v1.2.3 From 8c22e6a640d458c5447fd8a841a6694f077864e4 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 14 Nov 2023 22:39:33 -0500 Subject: use start time and endtime for clips --- src/invidious/routes/watch.cr | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) (limited to 'src') diff --git a/src/invidious/routes/watch.cr b/src/invidious/routes/watch.cr index 3d935f0a..1b1169d8 100644 --- a/src/invidious/routes/watch.cr +++ b/src/invidious/routes/watch.cr @@ -275,6 +275,22 @@ module Invidious::Routes::Watch return error_template(400, "Invalid clip ID") if response["error"]? if video_id = response.dig?("endpoint", "watchEndpoint", "videoId") + if params = response.dig?("endpoint", "watchEndpoint", "params").try &.as_s + decoded_protobuf = params.try { |i| URI.decode_www_form(i) } + .try { |i| Base64.decode(i) } + .try { |i| IO::Memory.new(i) } + .try { |i| Protodec::Any.parse(i) } + + start_time = decoded_protobuf + .try(&.["50:0:embedded"]["2:1:varint"].as_i64) + + end_time = decoded_protobuf + .try(&.["50:0:embedded"]["3:2:varint"].as_i64) + + env.params.query["start"] = (start_time / 1000).to_s + env.params.query["end"] = (end_time / 1000).to_s + end + return env.redirect "/watch?v=#{video_id}&#{env.params.query}" else return error_template(404, "The requested clip doesn't exist") -- cgit v1.2.3 From b344d98c25185ca4e163eeda128b9fc68ff865b6 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 14 Nov 2023 23:35:11 -0500 Subject: Add API endpoint for Clips --- src/invidious/routes/api/v1/videos.cr | 43 +++++++++++++++++++++++++++++++++++ src/invidious/routes/watch.cr | 16 +++---------- src/invidious/routing.cr | 1 + src/invidious/videos/clip.cr | 20 ++++++++++++++++ 4 files changed, 67 insertions(+), 13 deletions(-) create mode 100644 src/invidious/videos/clip.cr (limited to 'src') diff --git a/src/invidious/routes/api/v1/videos.cr b/src/invidious/routes/api/v1/videos.cr index 1017ac9d..9281f4dd 100644 --- a/src/invidious/routes/api/v1/videos.cr +++ b/src/invidious/routes/api/v1/videos.cr @@ -363,4 +363,47 @@ module Invidious::Routes::API::V1::Videos end end end + + def self.clips(env) + locale = env.get("preferences").as(Preferences).locale + + env.response.content_type = "application/json" + + clip_id = env.params.url["id"] + region = env.params.query["region"]? + proxy = {"1", "true"}.any? &.== env.params.query["local"]? + + response = YoutubeAPI.resolve_url("https://www.youtube.com/clip/#{clip_id}") + return error_json(400, "Invalid clip ID") if response["error"]? + + video_id = response.dig?("endpoint", "watchEndpoint", "videoId").try &.as_s + return error_json(400, "Invalid clip ID") if video_id.nil? + + start_time = nil + end_time = nil + clip_title = nil + + if params = response.dig?("endpoint", "watchEndpoint", "params").try &.as_s + start_time, end_time, clip_title = parse_clip_parameters(params) + end + + begin + video = get_video(video_id, region: region) + rescue ex : NotFoundException + return error_json(404, ex) + rescue ex + return error_json(500, ex) + end + + return JSON.build do |json| + json.object do + json.field "startTime", start_time + json.field "endTime", end_time + json.field "clipTitle", clip_title + json.field "video" do + Invidious::JSONify::APIv1.video(video, json, locale: locale, proxy: proxy) + end + end + end + end end diff --git a/src/invidious/routes/watch.cr b/src/invidious/routes/watch.cr index 1b1169d8..1cba86f6 100644 --- a/src/invidious/routes/watch.cr +++ b/src/invidious/routes/watch.cr @@ -276,19 +276,9 @@ module Invidious::Routes::Watch if video_id = response.dig?("endpoint", "watchEndpoint", "videoId") if params = response.dig?("endpoint", "watchEndpoint", "params").try &.as_s - decoded_protobuf = params.try { |i| URI.decode_www_form(i) } - .try { |i| Base64.decode(i) } - .try { |i| IO::Memory.new(i) } - .try { |i| Protodec::Any.parse(i) } - - start_time = decoded_protobuf - .try(&.["50:0:embedded"]["2:1:varint"].as_i64) - - end_time = decoded_protobuf - .try(&.["50:0:embedded"]["3:2:varint"].as_i64) - - env.params.query["start"] = (start_time / 1000).to_s - env.params.query["end"] = (end_time / 1000).to_s + start_time, end_time, _ = parse_clip_parameters(params) + env.params.query["start"] = start_time.to_s + env.params.query["end"] = end_time.to_s end return env.redirect "/watch?v=#{video_id}&#{env.params.query}" diff --git a/src/invidious/routing.cr b/src/invidious/routing.cr index d6bd991c..ba05da19 100644 --- a/src/invidious/routing.cr +++ b/src/invidious/routing.cr @@ -235,6 +235,7 @@ module Invidious::Routing get "/api/v1/captions/:id", {{namespace}}::Videos, :captions get "/api/v1/annotations/:id", {{namespace}}::Videos, :annotations get "/api/v1/comments/:id", {{namespace}}::Videos, :comments + get "/api/v1/clips/:id", {{namespace}}::Videos, :clips # Feeds get "/api/v1/trending", {{namespace}}::Feeds, :trending diff --git a/src/invidious/videos/clip.cr b/src/invidious/videos/clip.cr new file mode 100644 index 00000000..47f108a3 --- /dev/null +++ b/src/invidious/videos/clip.cr @@ -0,0 +1,20 @@ +require "json" + +# returns start_time, end_time and clip_title +def parse_clip_parameters(params) : {Float64, Float64, String} + decoded_protobuf = params.try { |i| URI.decode_www_form(i) } + .try { |i| Base64.decode(i) } + .try { |i| IO::Memory.new(i) } + .try { |i| Protodec::Any.parse(i) } + + start_time = decoded_protobuf + .try(&.["50:0:embedded"]["2:1:varint"].as_i64) + + end_time = decoded_protobuf + .try(&.["50:0:embedded"]["3:2:varint"].as_i64) + + clip_title = decoded_protobuf + .try(&.["50:0:embedded"]["4:3:string"].as_s) + + return (start_time / 1000), (end_time / 1000), clip_title +end -- cgit v1.2.3 From b5f8b4542aea3d0bb56dc682058cfdf8e666099e Mon Sep 17 00:00:00 2001 From: Chunky programmer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Wed, 7 Jun 2023 16:04:14 -0400 Subject: Search: Don't error if AuthorId does not exist --- src/invidious/views/components/item.ecr | 36 ++++++++++++++++++++++++--------- src/invidious/yt_backend/extractors.cr | 4 ++-- 2 files changed, 28 insertions(+), 12 deletions(-) (limited to 'src') diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr index 031b46da..6d227cfc 100644 --- a/src/invidious/views/components/item.ecr +++ b/src/invidious/views/components/item.ecr @@ -82,11 +82,19 @@
- +
+ <% if !item.ucid.to_s.empty? %> + +

<%= HTML.escape(item.author) %> + <%- if author_verified %> <% end -%> +

+
+ <% else %> +

<%= HTML.escape(item.author) %> + <%- if author_verified %> <% end -%> +

+ <% end %> +
<% when Category %> <% else %> @@ -160,11 +168,19 @@
- +
+ <% if !item.ucid.to_s.empty? %> + +

<%= HTML.escape(item.author) %> + <%- if author_verified %> <% end -%> +

+
+ <% else %> +

<%= HTML.escape(item.author) %> + <%- if author_verified %> <% end -%> +

+ <% end %> +
<%= rendered "components/video-context-buttons" %>
diff --git a/src/invidious/yt_backend/extractors.cr b/src/invidious/yt_backend/extractors.cr index 56325cf7..0e72957e 100644 --- a/src/invidious/yt_backend/extractors.cr +++ b/src/invidious/yt_backend/extractors.cr @@ -822,9 +822,9 @@ module HelperExtractors end # Retrieves the ID required for querying the InnerTube browse endpoint. - # Raises when it's unable to do so + # Returns an empty string when it's unable to do so def self.get_browse_id(container) - return container.dig("navigationEndpoint", "browseEndpoint", "browseId").as_s + return container.dig?("navigationEndpoint", "browseEndpoint", "browseId").try &.as_s || "" end end -- cgit v1.2.3 From f1edb1d6bfbc30af46f6d3fa291d6d6fd7d66819 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 17 Oct 2023 17:49:32 -0400 Subject: fix related video author when id is empty --- src/invidious/views/watch.ecr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 07474896..1b020321 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -346,7 +346,7 @@ we're going to need to do it here in order to allow for translations.
- <% if rv["ucid"]? %> + <% if !rv["ucid"].empty? %> "><%= rv["author"]? %><% if rv["author_verified"]? == "true" %> <% end %> <% else %> <%= rv["author"]? %><% if rv["author_verified"]? == "true" %> <% end %> -- cgit v1.2.3 From fe8b1b4cc4fca50de0ff5891ae5f0742d13c5d41 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Thu, 7 Dec 2023 11:43:56 -0500 Subject: Add title to toggle theme icon --- locales/en-US.json | 3 ++- src/invidious/views/template.ecr | 21 ++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) (limited to 'src') diff --git a/locales/en-US.json b/locales/en-US.json index a9f78165..227b0677 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -487,5 +487,6 @@ "channel_tab_releases_label": "Releases", "channel_tab_playlists_label": "Playlists", "channel_tab_community_label": "Community", - "channel_tab_channels_label": "Channels" + "channel_tab_channels_label": "Channels", + "toggle_theme": "Toggle Theme" } diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr index 77265679..fd755619 100644 --- a/src/invidious/views/template.ecr +++ b/src/invidious/views/template.ecr @@ -1,5 +1,9 @@ +<% + locale = env.get("preferences").as(Preferences).locale + dark_mode = env.get("preferences").as(Preferences).dark_mode +%> -"> + @@ -20,13 +24,8 @@ -<% - locale = env.get("preferences").as(Preferences).locale - dark_mode = env.get("preferences").as(Preferences).dark_mode -%> - -theme"> - +
@@ -43,8 +42,8 @@
<% if env.get? "user" %> <% else %>
- " class="pure-menu-heading"> - <% if env.get("preferences").as(Preferences).dark_mode == "dark" %> + " class="pure-menu-heading" title="<%= translate(locale, "toggle_theme") %>"> + <% if dark_mode == "dark" %> <% else %> -- cgit v1.2.3 From 87a8207f370f20582d6330d9fcf4346fbb4e1ae5 Mon Sep 17 00:00:00 2001 From: guidiasz Date: Mon, 18 Dec 2023 13:23:55 -0300 Subject: fix: "Watch on YouTube" preserve current playlist --- src/invidious/views/watch.ecr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index 07474896..cce6115a 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -118,7 +118,7 @@ we're going to need to do it here in order to allow for translations. link_yt_embed = URI.new(scheme: "https", host: "www.youtube.com", path: "/embed/#{video.id}") if !plid.nil? && !continuation.nil? - link_yt_param = URI::Params{"plid" => [plid], "index" => [continuation.to_s]} + link_yt_param = URI::Params{"list" => [plid], "index" => [continuation.to_s]} link_yt_watch = IV::HttpServer::Utils.add_params_to_url(link_yt_watch, link_yt_param) link_yt_embed = IV::HttpServer::Utils.add_params_to_url(link_yt_embed, link_yt_param) end -- cgit v1.2.3 From 090b470bfcadce192439500ff89598fc6ba3faac Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 19 Dec 2023 23:07:18 -0500 Subject: fix potential memory leak --- src/invidious/routes/api/v1/search.cr | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/invidious/routes/api/v1/search.cr b/src/invidious/routes/api/v1/search.cr index a65571ea..2922b060 100644 --- a/src/invidious/routes/api/v1/search.cr +++ b/src/invidious/routes/api/v1/search.cr @@ -37,6 +37,7 @@ module Invidious::Routes::API::V1::Search url = "/complete/search?client=youtube&hl=en&gl=#{region}&q=#{URI.encode_www_form(query)}&gs_ri=youtube&ds=yt" response = client.get(url).body + client.close body = JSON.parse(response[19..-2]).as_a suggestions = body[1].as_a[0..-2] -- cgit v1.2.3 From 0917efd9cbf4129d508217dbf38c98db5eba13cf Mon Sep 17 00:00:00 2001 From: nixos script Date: Thu, 21 Dec 2023 13:50:32 +0800 Subject: fix issue where scope would be missing the * if the user was not logged in before calling the authorize endpoint fix #4200 --- src/invidious/helpers/utils.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index a006d602..e438e3b9 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -262,7 +262,7 @@ def get_referer(env, fallback = "/", unroll = true) end referer = referer.request_target - referer = "/" + referer.gsub(/[^\/?@&%=\-_.:,0-9a-zA-Z]/, "").lstrip("/\\") + referer = "/" + referer.gsub(/[^\/?@&%=\-_.:,*0-9a-zA-Z]/, "").lstrip("/\\") if referer == env.request.path referer = fallback -- cgit v1.2.3 From 7da4a7f72b0e328f72aff884605a21c4ffe7cb04 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Tue, 19 Dec 2023 22:37:48 -0500 Subject: add null safety to clip parsing --- src/invidious/routes/watch.cr | 4 ++-- src/invidious/videos/clip.cr | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/invidious/routes/watch.cr b/src/invidious/routes/watch.cr index 1cba86f6..aabe8dfc 100644 --- a/src/invidious/routes/watch.cr +++ b/src/invidious/routes/watch.cr @@ -277,8 +277,8 @@ module Invidious::Routes::Watch if video_id = response.dig?("endpoint", "watchEndpoint", "videoId") if params = response.dig?("endpoint", "watchEndpoint", "params").try &.as_s start_time, end_time, _ = parse_clip_parameters(params) - env.params.query["start"] = start_time.to_s - env.params.query["end"] = end_time.to_s + env.params.query["start"] = start_time.to_s if start_time != nil + env.params.query["end"] = end_time.to_s if end_time != nil end return env.redirect "/watch?v=#{video_id}&#{env.params.query}" diff --git a/src/invidious/videos/clip.cr b/src/invidious/videos/clip.cr index 47f108a3..29c57182 100644 --- a/src/invidious/videos/clip.cr +++ b/src/invidious/videos/clip.cr @@ -1,7 +1,7 @@ require "json" # returns start_time, end_time and clip_title -def parse_clip_parameters(params) : {Float64, Float64, String} +def parse_clip_parameters(params) : {Float64?, Float64?, String?} decoded_protobuf = params.try { |i| URI.decode_www_form(i) } .try { |i| Base64.decode(i) } .try { |i| IO::Memory.new(i) } @@ -9,12 +9,14 @@ def parse_clip_parameters(params) : {Float64, Float64, String} start_time = decoded_protobuf .try(&.["50:0:embedded"]["2:1:varint"].as_i64) + .try { |i| i/1000 } end_time = decoded_protobuf .try(&.["50:0:embedded"]["3:2:varint"].as_i64) + .try { |i| i/1000 } clip_title = decoded_protobuf .try(&.["50:0:embedded"]["4:3:string"].as_s) - return (start_time / 1000), (end_time / 1000), clip_title + return start_time, end_time, clip_title end -- cgit v1.2.3 From 7cca1285aaa1463eb31f82e49f903b437b4de69f Mon Sep 17 00:00:00 2001 From: vojkovic Date: Sat, 6 Jan 2024 15:51:31 +0800 Subject: Fix two swapped function names --- src/invidious/database/statistics.cr | 4 ++-- src/invidious/jobs/statistics_refresh_job.cr | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/invidious/database/statistics.cr b/src/invidious/database/statistics.cr index 1df549e2..9e4963fd 100644 --- a/src/invidious/database/statistics.cr +++ b/src/invidious/database/statistics.cr @@ -15,7 +15,7 @@ module Invidious::Database::Statistics PG_DB.query_one(request, as: Int64) end - def count_users_active_1m : Int64 + def count_users_active_6m : Int64 request = <<-SQL SELECT count(*) FROM users WHERE CURRENT_TIMESTAMP - updated < '6 months' @@ -24,7 +24,7 @@ module Invidious::Database::Statistics PG_DB.query_one(request, as: Int64) end - def count_users_active_6m : Int64 + def count_users_active_1m : Int64 request = <<-SQL SELECT count(*) FROM users WHERE CURRENT_TIMESTAMP - updated < '1 month' diff --git a/src/invidious/jobs/statistics_refresh_job.cr b/src/invidious/jobs/statistics_refresh_job.cr index 72d1ce88..66c91ad5 100644 --- a/src/invidious/jobs/statistics_refresh_job.cr +++ b/src/invidious/jobs/statistics_refresh_job.cr @@ -56,8 +56,8 @@ class Invidious::Jobs::StatisticsRefreshJob < Invidious::Jobs::BaseJob users = STATISTICS.dig("usage", "users").as(Hash(String, Int64)) users["total"] = Invidious::Database::Statistics.count_users_total - users["activeHalfyear"] = Invidious::Database::Statistics.count_users_active_1m - users["activeMonth"] = Invidious::Database::Statistics.count_users_active_6m + users["activeHalfyear"] = Invidious::Database::Statistics.count_users_active_6m + users["activeMonth"] = Invidious::Database::Statistics.count_users_active_1m STATISTICS["metadata"] = { "updatedAt" => Time.utc.to_unix, -- cgit v1.2.3 From 1c0b4205d40781ff2d34d64dddf29e5dc89d1723 Mon Sep 17 00:00:00 2001 From: syeopite <70992037+syeopite@users.noreply.github.com> Date: Wed, 10 Jan 2024 23:01:00 +0000 Subject: Add parameter to disable `force_resolve` in `make_client` (#4335) * Add option to disable force_resolve in make_client Some websites such as archive.org and textcaptcha.com does not support IPv6 and as such requests fail when Invidious requests with IPv6 to those services. * Reenable force_resolve on pubsub subcribe request * Make force_resolve false by default in make_client * Remove missed explicit force_resolve=false --- src/invidious/routes/video_playback.cr | 8 ++++---- src/invidious/yt_backend/connection_pool.cr | 15 ++++++++++----- 2 files changed, 14 insertions(+), 9 deletions(-) (limited to 'src') diff --git a/src/invidious/routes/video_playback.cr b/src/invidious/routes/video_playback.cr index 1d5aa914..ec18f3b8 100644 --- a/src/invidious/routes/video_playback.cr +++ b/src/invidious/routes/video_playback.cr @@ -42,7 +42,7 @@ module Invidious::Routes::VideoPlayback headers["Range"] = "bytes=#{range_for_head}" end - client = make_client(URI.parse(host), region) + client = make_client(URI.parse(host), region, force_resolve = true) response = HTTP::Client::Response.new(500) error = "" 5.times do @@ -57,7 +57,7 @@ module Invidious::Routes::VideoPlayback if new_host != host host = new_host client.close - client = make_client(URI.parse(new_host), region) + client = make_client(URI.parse(new_host), region, force_resolve = true) end url = "#{location.request_target}&host=#{location.host}#{region ? "®ion=#{region}" : ""}" @@ -71,7 +71,7 @@ module Invidious::Routes::VideoPlayback fvip = "3" host = "https://r#{fvip}---#{mn}.googlevideo.com" - client = make_client(URI.parse(host), region) + client = make_client(URI.parse(host), region, force_resolve = true) rescue ex error = ex.message end @@ -196,7 +196,7 @@ module Invidious::Routes::VideoPlayback break else client.close - client = make_client(URI.parse(host), region) + client = make_client(URI.parse(host), region, force_resolve = true) end end diff --git a/src/invidious/yt_backend/connection_pool.cr b/src/invidious/yt_backend/connection_pool.cr index 36e82766..81cfb272 100644 --- a/src/invidious/yt_backend/connection_pool.cr +++ b/src/invidious/yt_backend/connection_pool.cr @@ -26,7 +26,7 @@ struct YoutubeConnectionPool def client(region = nil, &block) if region - conn = make_client(url, region) + conn = make_client(url, region, force_resolve = true) response = yield conn else conn = pool.checkout @@ -59,9 +59,14 @@ struct YoutubeConnectionPool end end -def make_client(url : URI, region = nil) +def make_client(url : URI, region = nil, force_resolve : Bool = false) client = HTTPClient.new(url, OpenSSL::SSL::Context::Client.insecure) - client.family = CONFIG.force_resolve + + # Some services do not support IPv6. + if force_resolve + client.family = CONFIG.force_resolve + end + client.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com" client.read_timeout = 10.seconds client.connect_timeout = 10.seconds @@ -80,8 +85,8 @@ def make_client(url : URI, region = nil) return client end -def make_client(url : URI, region = nil, &block) - client = make_client(url, region) +def make_client(url : URI, region = nil, force_resolve : Bool = false, &block) + client = make_client(url, region, force_resolve) begin yield client ensure -- cgit v1.2.3 From 4a339df5c49e30a5ef5008d26639eb69edfff152 Mon Sep 17 00:00:00 2001 From: toabr <25079664+toabr@users.noreply.github.com> Date: Sat, 27 Jan 2024 00:38:47 +0100 Subject: CSS: expand #contents width on small screens --- assets/css/default.css | 1 + src/invidious/views/template.ecr | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/assets/css/default.css b/assets/css/default.css index 00881253..fd696178 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -13,6 +13,7 @@ body { display: flex; flex-direction: column; min-height: 100vh; + margin: auto; } .h-box { diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr index 77265679..5e2cf88e 100644 --- a/src/invidious/views/template.ecr +++ b/src/invidious/views/template.ecr @@ -28,8 +28,7 @@ -theme">
-
-
+
-- cgit v1.2.3 From c005ada48723808e507d0a4d5a3363a1c14a4f07 Mon Sep 17 00:00:00 2001 From: ThetaDev Date: Mon, 29 Jan 2024 14:59:25 +0100 Subject: fix: prevent censoring of self-harm related search queries (#4403) * fix: prevent censoring of self-harm related search queries * fix: yt_filters_spec with new flag --- spec/invidious/search/yt_filters_spec.cr | 54 ++++++++++++++++---------------- src/invidious/search/filters.cr | 6 ++-- 2 files changed, 30 insertions(+), 30 deletions(-) (limited to 'src') diff --git a/spec/invidious/search/yt_filters_spec.cr b/spec/invidious/search/yt_filters_spec.cr index bf7f21e7..8abed5ce 100644 --- a/spec/invidious/search/yt_filters_spec.cr +++ b/spec/invidious/search/yt_filters_spec.cr @@ -12,45 +12,45 @@ end # page of Youtube with any browser devtools HTML inspector. DATE_FILTERS = { - Invidious::Search::Filters::Date::Hour => "EgIIAQ%3D%3D", - Invidious::Search::Filters::Date::Today => "EgIIAg%3D%3D", - Invidious::Search::Filters::Date::Week => "EgIIAw%3D%3D", - Invidious::Search::Filters::Date::Month => "EgIIBA%3D%3D", - Invidious::Search::Filters::Date::Year => "EgIIBQ%3D%3D", + Invidious::Search::Filters::Date::Hour => "EgIIAfABAQ%3D%3D", + Invidious::Search::Filters::Date::Today => "EgIIAvABAQ%3D%3D", + Invidious::Search::Filters::Date::Week => "EgIIA_ABAQ%3D%3D", + Invidious::Search::Filters::Date::Month => "EgIIBPABAQ%3D%3D", + Invidious::Search::Filters::Date::Year => "EgIIBfABAQ%3D%3D", } TYPE_FILTERS = { - Invidious::Search::Filters::Type::Video => "EgIQAQ%3D%3D", - Invidious::Search::Filters::Type::Channel => "EgIQAg%3D%3D", - Invidious::Search::Filters::Type::Playlist => "EgIQAw%3D%3D", - Invidious::Search::Filters::Type::Movie => "EgIQBA%3D%3D", + Invidious::Search::Filters::Type::Video => "EgIQAfABAQ%3D%3D", + Invidious::Search::Filters::Type::Channel => "EgIQAvABAQ%3D%3D", + Invidious::Search::Filters::Type::Playlist => "EgIQA_ABAQ%3D%3D", + Invidious::Search::Filters::Type::Movie => "EgIQBPABAQ%3D%3D", } DURATION_FILTERS = { - Invidious::Search::Filters::Duration::Short => "EgIYAQ%3D%3D", - Invidious::Search::Filters::Duration::Medium => "EgIYAw%3D%3D", - Invidious::Search::Filters::Duration::Long => "EgIYAg%3D%3D", + Invidious::Search::Filters::Duration::Short => "EgIYAfABAQ%3D%3D", + Invidious::Search::Filters::Duration::Medium => "EgIYA_ABAQ%3D%3D", + Invidious::Search::Filters::Duration::Long => "EgIYAvABAQ%3D%3D", } FEATURE_FILTERS = { - Invidious::Search::Filters::Features::Live => "EgJAAQ%3D%3D", - Invidious::Search::Filters::Features::FourK => "EgJwAQ%3D%3D", - Invidious::Search::Filters::Features::HD => "EgIgAQ%3D%3D", - Invidious::Search::Filters::Features::Subtitles => "EgIoAQ%3D%3D", - Invidious::Search::Filters::Features::CCommons => "EgIwAQ%3D%3D", - Invidious::Search::Filters::Features::ThreeSixty => "EgJ4AQ%3D%3D", - Invidious::Search::Filters::Features::VR180 => "EgPQAQE%3D", - Invidious::Search::Filters::Features::ThreeD => "EgI4AQ%3D%3D", - Invidious::Search::Filters::Features::HDR => "EgPIAQE%3D", - Invidious::Search::Filters::Features::Location => "EgO4AQE%3D", - Invidious::Search::Filters::Features::Purchased => "EgJIAQ%3D%3D", + Invidious::Search::Filters::Features::Live => "EgJAAfABAQ%3D%3D", + Invidious::Search::Filters::Features::FourK => "EgJwAfABAQ%3D%3D", + Invidious::Search::Filters::Features::HD => "EgIgAfABAQ%3D%3D", + Invidious::Search::Filters::Features::Subtitles => "EgIoAfABAQ%3D%3D", + Invidious::Search::Filters::Features::CCommons => "EgIwAfABAQ%3D%3D", + Invidious::Search::Filters::Features::ThreeSixty => "EgJ4AfABAQ%3D%3D", + Invidious::Search::Filters::Features::VR180 => "EgPQAQHwAQE%3D", + Invidious::Search::Filters::Features::ThreeD => "EgI4AfABAQ%3D%3D", + Invidious::Search::Filters::Features::HDR => "EgPIAQHwAQE%3D", + Invidious::Search::Filters::Features::Location => "EgO4AQHwAQE%3D", + Invidious::Search::Filters::Features::Purchased => "EgJIAfABAQ%3D%3D", } SORT_FILTERS = { - Invidious::Search::Filters::Sort::Relevance => "", - Invidious::Search::Filters::Sort::Date => "CAI%3D", - Invidious::Search::Filters::Sort::Views => "CAM%3D", - Invidious::Search::Filters::Sort::Rating => "CAE%3D", + Invidious::Search::Filters::Sort::Relevance => "8AEB", + Invidious::Search::Filters::Sort::Date => "CALwAQE%3D", + Invidious::Search::Filters::Sort::Views => "CAPwAQE%3D", + Invidious::Search::Filters::Sort::Rating => "CAHwAQE%3D", } Spectator.describe Invidious::Search::Filters do diff --git a/src/invidious/search/filters.cr b/src/invidious/search/filters.cr index c2b5c758..bf968734 100644 --- a/src/invidious/search/filters.cr +++ b/src/invidious/search/filters.cr @@ -300,9 +300,9 @@ module Invidious::Search object["9:varint"] = ((page - 1) * 20).to_i64 end - # If the object is empty, return an empty string, - # otherwise encode to protobuf then to base64 - return "" if object.empty? + # Prevent censoring of self harm topics + # See https://github.com/iv-org/invidious/issues/4398 + object["30:varint"] = 1.to_i64 return object .try { |i| Protodec::Any.cast_json(i) } -- cgit v1.2.3 From 0ad2eff2a46c28a877de1960a2dc5c15c0f94444 Mon Sep 17 00:00:00 2001 From: syeopite Date: Tue, 30 Jan 2024 15:25:45 -0800 Subject: WebVTT::Builder: Add logic to escape special chars --- spec/helpers/vtt/builder_spec.cr | 65 +++++++++++++++++++++++++++------------- src/invidious/helpers/webvtt.cr | 16 +++++++++- 2 files changed, 59 insertions(+), 22 deletions(-) (limited to 'src') diff --git a/spec/helpers/vtt/builder_spec.cr b/spec/helpers/vtt/builder_spec.cr index 7b543ddc..dc1f4613 100644 --- a/spec/helpers/vtt/builder_spec.cr +++ b/spec/helpers/vtt/builder_spec.cr @@ -1,34 +1,27 @@ require "../../spec_helper.cr" -MockLines = [ - { - "start_time": Time::Span.new(seconds: 1), - "end_time": Time::Span.new(seconds: 2), - "text": "Line 1", - }, - - { - "start_time": Time::Span.new(seconds: 2), - "end_time": Time::Span.new(seconds: 3), - "text": "Line 2", - }, -] +MockLines = ["Line 1", "Line 2"] +MockLinesWithEscapableCharacter = ["", "&Line 2>", '\u200E' + "Line\u200F 3", "\u00A0Line 4"] Spectator.describe "WebVTT::Builder" do it "correctly builds a vtt file" do result = WebVTT.build do |vtt| - MockLines.each do |line| - vtt.cue(line["start_time"], line["end_time"], line["text"]) + 2.times do |i| + vtt.cue( + Time::Span.new(seconds: i), + Time::Span.new(seconds: i + 1), + MockLines[i] + ) end end expect(result).to eq([ "WEBVTT", "", - "00:00:01.000 --> 00:00:02.000", + "00:00:00.000 --> 00:00:01.000", "Line 1", "", - "00:00:02.000 --> 00:00:03.000", + "00:00:01.000 --> 00:00:02.000", "Line 2", "", "", @@ -42,8 +35,12 @@ Spectator.describe "WebVTT::Builder" do } result = WebVTT.build(setting_fields) do |vtt| - MockLines.each do |line| - vtt.cue(line["start_time"], line["end_time"], line["text"]) + 2.times do |i| + vtt.cue( + Time::Span.new(seconds: i), + Time::Span.new(seconds: i + 1), + MockLines[i] + ) end end @@ -52,13 +49,39 @@ Spectator.describe "WebVTT::Builder" do "Kind: captions", "Language: en", "", - "00:00:01.000 --> 00:00:02.000", + "00:00:00.000 --> 00:00:01.000", "Line 1", "", - "00:00:02.000 --> 00:00:03.000", + "00:00:01.000 --> 00:00:02.000", "Line 2", "", "", ].join('\n')) end + + it "properly escapes characters" do + result = WebVTT.build do |vtt| + 4.times do |i| + vtt.cue(Time::Span.new(seconds: i), Time::Span.new(seconds: i + 1), MockLinesWithEscapableCharacter[i]) + end + end + + expect(result).to eq([ + "WEBVTT", + "", + "00:00:00.000 --> 00:00:01.000", + "<Line 1>", + "", + "00:00:01.000 --> 00:00:02.000", + "&Line 2>", + "", + "00:00:02.000 --> 00:00:03.000", + "‎Line‏ 3", + "", + "00:00:03.000 --> 00:00:04.000", + " Line 4", + "", + "", + ].join('\n')) + end end diff --git a/src/invidious/helpers/webvtt.cr b/src/invidious/helpers/webvtt.cr index 56f761ed..260d250f 100644 --- a/src/invidious/helpers/webvtt.cr +++ b/src/invidious/helpers/webvtt.cr @@ -4,13 +4,23 @@ module WebVTT # A WebVTT builder generates WebVTT files private class Builder + # See https://developer.mozilla.org/en-US/docs/Web/API/WebVTT_API#cue_payload + private ESCAPE_SUBSTITUTIONS = { + '&' => "&", + '<' => "<", + '>' => ">", + '\u200E' => "‎", + '\u200F' => "‏", + '\u00A0' => " ", + } + def initialize(@io : IO) end # Writes an vtt cue with the specified time stamp and contents def cue(start_time : Time::Span, end_time : Time::Span, text : String) timestamp(start_time, end_time) - @io << text + @io << self.escape(text) @io << "\n\n" end @@ -29,6 +39,10 @@ module WebVTT @io << '.' << timestamp.milliseconds.to_s.rjust(3, '0') end + private def escape(text : String) : String + return text.gsub(ESCAPE_SUBSTITUTIONS) + end + def document(setting_fields : Hash(String, String)? = nil, &) @io << "WEBVTT\n" -- cgit v1.2.3 From c864a63b6d86cb7552d2f1730731e427fc435fe4 Mon Sep 17 00:00:00 2001 From: shironeko Date: Thu, 8 Feb 2024 17:05:11 -0500 Subject: Fix pubsub feed parsing similar to what's done in #3793, this is causing an assert on my instance --- src/invidious/routes/feeds.cr | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/invidious/routes/feeds.cr b/src/invidious/routes/feeds.cr index 40bca008..512d4ee7 100644 --- a/src/invidious/routes/feeds.cr +++ b/src/invidious/routes/feeds.cr @@ -407,12 +407,17 @@ module Invidious::Routes::Feeds end spawn do - rss = XML.parse_html(body) - rss.xpath_nodes("//feed/entry").each do |entry| - id = entry.xpath_node("videoid").not_nil!.content - author = entry.xpath_node("author/name").not_nil!.content - published = Time.parse_rfc3339(entry.xpath_node("published").not_nil!.content) - updated = Time.parse_rfc3339(entry.xpath_node("updated").not_nil!.content) + # TODO: unify this with the other almost identical looking parts in this and channels.cr somehow? + namespaces = { + "yt" => "http://www.youtube.com/xml/schemas/2015", + "default" => "http://www.w3.org/2005/Atom", + } + rss = XML.parse(body) + rss.xpath_nodes("//default:feed/default:entry", namespaces).each do |entry| + id = entry.xpath_node("yt:videoId", namespaces).not_nil!.content + author = entry.xpath_node("default:author/default:name", namespaces).not_nil!.content + published = Time.parse_rfc3339(entry.xpath_node("default:published", namespaces).not_nil!.content) + updated = Time.parse_rfc3339(entry.xpath_node("default:updated", namespaces).not_nil!.content) video = get_video(id, force_refresh: true) -- cgit v1.2.3 From 98c421e9f539f8d72e6842fea94d17ff0db7f38a Mon Sep 17 00:00:00 2001 From: shironeko Date: Thu, 8 Feb 2024 18:58:23 -0500 Subject: Fix when video from pubsub is a scheduled event --- src/invidious/routes/feeds.cr | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/invidious/routes/feeds.cr b/src/invidious/routes/feeds.cr index 512d4ee7..e20a7139 100644 --- a/src/invidious/routes/feeds.cr +++ b/src/invidious/routes/feeds.cr @@ -419,7 +419,11 @@ module Invidious::Routes::Feeds published = Time.parse_rfc3339(entry.xpath_node("default:published", namespaces).not_nil!.content) updated = Time.parse_rfc3339(entry.xpath_node("default:updated", namespaces).not_nil!.content) - video = get_video(id, force_refresh: true) + begin + video = get_video(id, force_refresh: true) + rescue + next # skip this video since it raised an exception (e.g. it is a scheduled live event) + end if CONFIG.enable_user_notifications # Deliver notifications to `/api/v1/auth/notifications` -- cgit v1.2.3 From 6b33820f1f13171c3b432d6bc548b23380a3790d Mon Sep 17 00:00:00 2001 From: shironeko Date: Thu, 8 Feb 2024 18:23:08 -0500 Subject: Add missing translation strings closes #3120 --- locales/en-US.json | 5 +++++ src/invidious/frontend/comments_reddit.cr | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/locales/en-US.json b/locales/en-US.json index a9f78165..29fc7db6 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -1,4 +1,9 @@ { + "Add to playlist": "Add to playlist", + "Add to playlist: ": "Add to playlist: ", + "Answer": "Answer", + "Search for videos": "Search for videos", + "The Popular feed has been disabled by the administrator.": "The Popular feed has been disabled by the administrator.", "generic_channels_count": "{{count}} channel", "generic_channels_count_plural": "{{count}} channels", "generic_views_count": "{{count}} view", diff --git a/src/invidious/frontend/comments_reddit.cr b/src/invidious/frontend/comments_reddit.cr index b5647bae..4dda683e 100644 --- a/src/invidious/frontend/comments_reddit.cr +++ b/src/invidious/frontend/comments_reddit.cr @@ -33,7 +33,7 @@ module Invidious::Frontend::Comments [ − ] #{child.author} #{translate_count(locale, "comments_points_count", child.score, NumberFormatting::Separator)} - #{translate(locale, "`x` ago", recode_date(child.created_utc, locale))} + #{translate(locale, "`x` ago", recode_date(child.created_utc, locale))} #{translate(locale, "permalink")}

-- cgit v1.2.3 From 7b84bdb29b60504c1c5c88617e191767803384ab Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Tue, 13 Feb 2024 21:05:26 +0100 Subject: API: Add APIHandler back This handler should no have been removed in 4276, as it adds the required CORS header (Access-Control-Allow-Origin) for public acces to the API. Thanks to iBicha for noticing this! --- src/invidious.cr | 1 + src/invidious/helpers/handlers.cr | 13 +++++++++++++ 2 files changed, 14 insertions(+) (limited to 'src') diff --git a/src/invidious.cr b/src/invidious.cr index c8cac80e..e0bd0101 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -217,6 +217,7 @@ public_folder "assets" Kemal.config.powered_by_header = false add_handler FilteredCompressHandler.new +add_handler APIHandler.new add_handler AuthHandler.new add_handler DenyFrame.new add_context_storage_type(Array(String)) diff --git a/src/invidious/helpers/handlers.cr b/src/invidious/helpers/handlers.cr index cece289b..174f620d 100644 --- a/src/invidious/helpers/handlers.cr +++ b/src/invidious/helpers/handlers.cr @@ -134,6 +134,19 @@ class AuthHandler < Kemal::Handler end end +class APIHandler < Kemal::Handler + {% for method in %w(GET POST PUT HEAD DELETE PATCH OPTIONS) %} + only ["/api/v1/*"], {{method}} + {% end %} + exclude ["/api/v1/auth/notifications"], "GET" + exclude ["/api/v1/auth/notifications"], "POST" + + def call(env) + env.response.headers["Access-Control-Allow-Origin"] = "*" if only_match?(env) + call_next env + end +end + class DenyFrame < Kemal::Handler exclude ["/embed/*"] -- cgit v1.2.3 From 60f6a345d926d615ff36284e9495864fc18703ef Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Thu, 15 Feb 2024 22:02:06 +0100 Subject: Locales: Fix broken i18Next v3/v4 plurals Languages impacted: es, fa, pt --- spec/i18next_plurals_spec.cr | 13 +++++++------ src/invidious/helpers/i18next.cr | 22 ++++++++++++---------- 2 files changed, 19 insertions(+), 16 deletions(-) (limited to 'src') diff --git a/spec/i18next_plurals_spec.cr b/spec/i18next_plurals_spec.cr index dab97710..dcd0f5ec 100644 --- a/spec/i18next_plurals_spec.cr +++ b/spec/i18next_plurals_spec.cr @@ -17,7 +17,7 @@ FORM_TESTS = { "cy" => I18next::Plurals::PluralForms::Special_Welsh, "fr" => I18next::Plurals::PluralForms::Special_French_Portuguese, "en" => I18next::Plurals::PluralForms::Single_not_one, - "es" => I18next::Plurals::PluralForms::Single_not_one, + "es" => I18next::Plurals::PluralForms::Special_Spanish_Italian, "ga" => I18next::Plurals::PluralForms::Special_Irish, "gd" => I18next::Plurals::PluralForms::Special_Scottish_Gaelic, "he" => I18next::Plurals::PluralForms::Special_Hebrew, @@ -33,7 +33,8 @@ FORM_TESTS = { "mt" => I18next::Plurals::PluralForms::Special_Maltese, "or" => I18next::Plurals::PluralForms::Special_Odia, "pl" => I18next::Plurals::PluralForms::Special_Polish_Kashubian, - "pt" => I18next::Plurals::PluralForms::Single_gt_one, + "pt" => I18next::Plurals::PluralForms::Special_French_Portuguese, + "pt-PT" => I18next::Plurals::PluralForms::Single_gt_one, "pt-BR" => I18next::Plurals::PluralForms::Special_French_Portuguese, "ro" => I18next::Plurals::PluralForms::Special_Romanian, "sk" => I18next::Plurals::PluralForms::Special_Czech_Slovak, @@ -77,10 +78,10 @@ SUFFIX_TESTS = { {num: 10, suffix: "_plural"}, ], "es" => [ - {num: 0, suffix: "_plural"}, - {num: 1, suffix: ""}, - {num: 10, suffix: "_plural"}, - {num: 6_000_000, suffix: "_plural"}, + {num: 0, suffix: "_2"}, + {num: 1, suffix: "_0"}, + {num: 10, suffix: "_2"}, + {num: 6_000_000, suffix: "_1"}, ], "fr" => [ {num: 0, suffix: "_0"}, diff --git a/src/invidious/helpers/i18next.cr b/src/invidious/helpers/i18next.cr index 252af6b9..9f4077e1 100644 --- a/src/invidious/helpers/i18next.cr +++ b/src/invidious/helpers/i18next.cr @@ -47,19 +47,19 @@ module I18next::Plurals private PLURAL_SETS = { PluralForms::Single_gt_one => [ - "ach", "ak", "am", "arn", "br", "fil", "gun", "ln", "mfe", "mg", - "mi", "oc", "pt", "tg", "tl", "ti", "tr", "uz", "wa", + "ach", "ak", "am", "arn", "br", "fa", "fil", "gun", "ln", "mfe", "mg", + "mi", "oc", "pt-PT", "tg", "tl", "ti", "tr", "uz", "wa", ], PluralForms::Single_not_one => [ "af", "an", "ast", "az", "bg", "bn", "ca", "da", "de", "dev", "el", "en", - "eo", "es", "et", "eu", "fi", "fo", "fur", "fy", "gl", "gu", "ha", "hi", + "eo", "et", "eu", "fi", "fo", "fur", "fy", "gl", "gu", "ha", "hi", "hu", "hy", "ia", "kk", "kn", "ku", "lb", "mai", "ml", "mn", "mr", "nah", "nap", "nb", "ne", "nl", "nn", "no", "nso", "pa", "pap", "pms", "ps", "rm", "sco", "se", "si", "so", "son", "sq", "sv", "sw", "ta", "te", "tk", "ur", "yo", ], PluralForms::None => [ - "ay", "bo", "cgg", "fa", "ht", "id", "ja", "jbo", "ka", "km", "ko", "ky", + "ay", "bo", "cgg", "ht", "id", "ja", "jbo", "ka", "km", "ko", "ky", "lo", "ms", "sah", "su", "th", "tt", "ug", "vi", "wo", "zh", ], PluralForms::Dual_Slavic => [ @@ -90,11 +90,13 @@ module I18next::Plurals "sk" => PluralForms::Special_Czech_Slovak, "sl" => PluralForms::Special_Slovenian, # Mixed v3/v4 rules - "fr" => PluralForms::Special_French_Portuguese, - "hr" => PluralForms::Special_Hungarian_Serbian, - "it" => PluralForms::Special_Spanish_Italian, - "pt-BR" => PluralForms::Special_French_Portuguese, - "sr" => PluralForms::Special_Hungarian_Serbian, + "es" => PluralForms::Special_Spanish_Italian, + "fr" => PluralForms::Special_French_Portuguese, + "hr" => PluralForms::Special_Hungarian_Serbian, + "it" => PluralForms::Special_Spanish_Italian, + "pt" => PluralForms::Special_French_Portuguese, + "pt" => PluralForms::Special_French_Portuguese, + "sr" => PluralForms::Special_Hungarian_Serbian, } # These are the v1 and v2 compatible suffixes. @@ -165,7 +167,7 @@ module I18next::Plurals def get_plural_form(locale : String) : PluralForms # Extract the ISO 639-1 or 639-2 code from an RFC 5646 language code - if !locale.matches?(/^pt-BR$/) + if !locale.matches?(/^pt-PT$/) locale = locale.split('-')[0] end -- cgit v1.2.3 From ef6b766b29160e06bd9abfb864851f993e75703c Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Mon, 29 Jan 2024 21:40:25 -0500 Subject: Add support for multi image community posts --- assets/css/carousel.css | 119 +++++++++++++++++++++++++++++ locales/en-US.json | 5 +- src/invidious/frontend/comments_youtube.cr | 30 ++++++++ src/invidious/helpers/i18n.cr | 7 +- src/invidious/views/template.ecr | 1 + 5 files changed, 160 insertions(+), 2 deletions(-) create mode 100644 assets/css/carousel.css (limited to 'src') diff --git a/assets/css/carousel.css b/assets/css/carousel.css new file mode 100644 index 00000000..8f0906d8 --- /dev/null +++ b/assets/css/carousel.css @@ -0,0 +1,119 @@ +/* +Copyright (c) 2024 by Jennifer (https://codepen.io/jwjertzoch/pen/JjyGeRy) + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall +be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +*/ + +.carousel { + margin: 0 auto; + overflow: hidden; + text-align: center; +} + +.slides { + width: 100%; + display: flex; + overflow-x: scroll; + scrollbar-width: none; + scroll-snap-type: x mandatory; + scroll-behavior: smooth; +} + +.slides::-webkit-scrollbar { + display: none; +} + +.slides-item { + align-items: center; + border-radius: 10px; + display: flex; + flex-shrink: 0; + font-size: 100px; + height: 600px; + justify-content: center; + margin: 0 1rem; + position: relative; + scroll-snap-align: start; + transform: scale(1); + transform-origin: center center; + transition: transform .5s; + width: 100%; +} + +.carousel__nav { + padding: 1.25rem .5rem; +} + +.slider-nav { + align-items: center; + background-color: #ddd; + border-radius: 50%; + color: #000; + display: inline-flex; + height: 1.5rem; + justify-content: center; + padding: .5rem; + position: relative; + text-decoration: none; + width: 1.5rem; +} + +.skip-link { + height: 1px; + overflow: hidden; + position: absolute; + top: auto; + width: 1px; +} + +.skip-link:focus { + align-items: center; + background-color: #000; + color: #fff; + display: flex; + font-size: 30px; + height: 30px; + justify-content: center; + opacity: .8; + text-decoration: none; + width: 50%; + z-index: 1; +} + +.light-theme .slider-nav { + background-color: #ddd; +} + +.dark-theme .slider-nav { + background-color: #0005; +} + +@media (prefers-color-scheme: light) { + .no-theme .slider-nav { + background-color: #ddd; + } +} + +@media (prefers-color-scheme: dark) { + .no-theme .slider-nav { + background-color: #0005; + } +} \ No newline at end of file diff --git a/locales/en-US.json b/locales/en-US.json index 227b0677..7899ba0a 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -488,5 +488,8 @@ "channel_tab_playlists_label": "Playlists", "channel_tab_community_label": "Community", "channel_tab_channels_label": "Channels", - "toggle_theme": "Toggle Theme" + "toggle_theme": "Toggle Theme", + "carousel_slide": "Slide {{current}} of {{total}}", + "carousel_skip": "Skip the Carousel", + "carousel_go_to": "Go to slide `x`" } diff --git a/src/invidious/frontend/comments_youtube.cr b/src/invidious/frontend/comments_youtube.cr index ecc0bc1b..6551d411 100644 --- a/src/invidious/frontend/comments_youtube.cr +++ b/src/invidious/frontend/comments_youtube.cr @@ -107,6 +107,36 @@ module Invidious::Frontend::Comments
END_HTML end + when "multiImage" + html << <<-END_HTML + + END_HTML else nil # Ignore end end diff --git a/src/invidious/helpers/i18n.cr b/src/invidious/helpers/i18n.cr index 76e477a4..8e2f7f44 100644 --- a/src/invidious/helpers/i18n.cr +++ b/src/invidious/helpers/i18n.cr @@ -78,7 +78,7 @@ def load_all_locales return locales end -def translate(locale : String?, key : String, text : String | Nil = nil) : String +def translate(locale : String?, key : String, text : String | Nil = nil, texts : Hash(String, String) | Nil = nil) : String # Log a warning if "key" doesn't exist in en-US locale and return # that key as the text, so this is more or less transparent to the user. if !LOCALES["en-US"].has_key?(key) @@ -116,6 +116,11 @@ def translate(locale : String?, key : String, text : String | Nil = nil) : Strin if text translation = translation.gsub("`x`", text) + elsif texts + # adds support for multi string interpolation. Based on i18next https://www.i18next.com/translation-function/interpolation#basic + texts.each_key do |hash_key| + translation = translation.gsub("{{#{hash_key}}}", texts[hash_key]) + end end return translation diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr index fd755619..379cf779 100644 --- a/src/invidious/views/template.ecr +++ b/src/invidious/views/template.ecr @@ -21,6 +21,7 @@ + -- cgit v1.2.3 From 26429bee3f2bede1d4270f6e71a52482be1d5d49 Mon Sep 17 00:00:00 2001 From: ChunkyProgrammer <78101139+ChunkyProgrammer@users.noreply.github.com> Date: Thu, 15 Feb 2024 21:44:40 -0500 Subject: make it so interpolation text can be a hash Co-Authored-By: Samantaz Fox --- src/invidious/frontend/comments_youtube.cr | 2 +- src/invidious/helpers/i18n.cr | 20 +++++++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/invidious/frontend/comments_youtube.cr b/src/invidious/frontend/comments_youtube.cr index 6551d411..aecac87f 100644 --- a/src/invidious/frontend/comments_youtube.cr +++ b/src/invidious/frontend/comments_youtube.cr @@ -117,7 +117,7 @@ module Invidious::Frontend::Comments image_array.each_index do |i| html << <<-END_HTML -
(i + 1).to_s, "total" => image_array.size.to_s})}" tabindex="0"> +
(i + 1).to_s, "total" => image_array.size.to_s})}" tabindex="0">
END_HTML diff --git a/src/invidious/helpers/i18n.cr b/src/invidious/helpers/i18n.cr index 8e2f7f44..23a1aafc 100644 --- a/src/invidious/helpers/i18n.cr +++ b/src/invidious/helpers/i18n.cr @@ -78,7 +78,7 @@ def load_all_locales return locales end -def translate(locale : String?, key : String, text : String | Nil = nil, texts : Hash(String, String) | Nil = nil) : String +def translate(locale : String?, key : String, text : String | Hash(String, String) | Nil = nil) : String # Log a warning if "key" doesn't exist in en-US locale and return # that key as the text, so this is more or less transparent to the user. if !LOCALES["en-US"].has_key?(key) @@ -101,10 +101,12 @@ def translate(locale : String?, key : String, text : String | Nil = nil, texts : match_length = 0 raw_data.as_h.each do |hash_key, value| - if md = text.try &.match(/#{hash_key}/) - if md[0].size >= match_length - translation = value.as_s - match_length = md[0].size + if text.is_a?(String) + if md = text.try &.match(/#{hash_key}/) + if md[0].size >= match_length + translation = value.as_s + match_length = md[0].size + end end end end @@ -114,12 +116,12 @@ def translate(locale : String?, key : String, text : String | Nil = nil, texts : raise "Invalid translation \"#{raw_data}\"" end - if text + if text.is_a?(String) translation = translation.gsub("`x`", text) - elsif texts + elsif text.is_a?(Hash(String, String)) # adds support for multi string interpolation. Based on i18next https://www.i18next.com/translation-function/interpolation#basic - texts.each_key do |hash_key| - translation = translation.gsub("{{#{hash_key}}}", texts[hash_key]) + text.each_key do |hash_key| + translation = translation.gsub("{{#{hash_key}}}", text[hash_key]) end end -- cgit v1.2.3 From 619aa3ff050573c119d9acf8302a4ddeb2beddc0 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Wed, 6 Mar 2024 20:46:50 +0100 Subject: YoutubeAPI: bump client versions --- src/invidious/yt_backend/youtube_api.cr | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) (limited to 'src') diff --git a/src/invidious/yt_backend/youtube_api.cr b/src/invidious/yt_backend/youtube_api.cr index a5e621f2..9e0631f6 100644 --- a/src/invidious/yt_backend/youtube_api.cr +++ b/src/invidious/yt_backend/youtube_api.cr @@ -7,17 +7,18 @@ module YoutubeAPI private DEFAULT_API_KEY = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8" - private ANDROID_APP_VERSION = "18.20.38" - # github.com/TeamNewPipe/NewPipeExtractor/blob/943b7c033bb9d07ead63ddab4441c287653e4384/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java#L1308 - private ANDROID_USER_AGENT = "com.google.android.youtube/18.20.38 (Linux; U; Android 12; US) gzip" + # For Android versions, see https://en.wikipedia.org/wiki/Android_version_history + private ANDROID_APP_VERSION = "19.09.36" + private ANDROID_USER_AGENT = "com.google.android.youtube/19.09.36 (Linux; U; Android 12; US) gzip" private ANDROID_SDK_VERSION = 31_i64 private ANDROID_VERSION = "12" - private IOS_APP_VERSION = "18.21.3" - # github.com/TeamNewPipe/NewPipeExtractor/blob/943b7c033bb9d07ead63ddab4441c287653e4384/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java#L1330 - private IOS_USER_AGENT = "com.google.ios.youtube/18.21.3 (iPhone14,5; U; CPU iOS 15_6 like Mac OS X;)" - # github.com/TeamNewPipe/NewPipeExtractor/blob/943b7c033bb9d07ead63ddab4441c287653e4384/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java#L1224 - private IOS_VERSION = "15.6.0.19G71" + # For Apple device names, see https://gist.github.com/adamawolf/3048717 + # For iOS versions, see https://en.wikipedia.org/wiki/IOS_version_history#Releases, + # then go to the dedicated article of the major version you want. + private IOS_APP_VERSION = "19.09.3" + private IOS_USER_AGENT = "com.google.ios.youtube/19.09.3 (iPhone14,5; U; CPU iOS 17_4 like Mac OS X;)" + private IOS_VERSION = "17.4.0.21E219" # Major.Minor.Patch.Build private WINDOWS_VERSION = "10.0" @@ -45,7 +46,7 @@ module YoutubeAPI ClientType::Web => { name: "WEB", name_proto: "1", - version: "2.20230602.01.00", + version: "2.20240304.00.00", api_key: DEFAULT_API_KEY, screen: "WATCH_FULL_SCREEN", os_name: "Windows", @@ -55,7 +56,7 @@ module YoutubeAPI ClientType::WebEmbeddedPlayer => { name: "WEB_EMBEDDED_PLAYER", name_proto: "56", - version: "1.20220803.01.00", + version: "1.20240303.00.00", api_key: DEFAULT_API_KEY, screen: "EMBED", os_name: "Windows", @@ -65,7 +66,7 @@ module YoutubeAPI ClientType::WebMobile => { name: "MWEB", name_proto: "2", - version: "2.20230531.05.00", + version: "2.20240304.08.00", api_key: DEFAULT_API_KEY, os_name: "Android", os_version: ANDROID_VERSION, @@ -74,7 +75,7 @@ module YoutubeAPI ClientType::WebScreenEmbed => { name: "WEB", name_proto: "1", - version: "2.20220804.00.00", + version: "2.20240304.00.00", api_key: DEFAULT_API_KEY, screen: "EMBED", os_name: "Windows", @@ -99,7 +100,7 @@ module YoutubeAPI name: "ANDROID_EMBEDDED_PLAYER", name_proto: "55", version: ANDROID_APP_VERSION, - api_key: DEFAULT_API_KEY, + api_key: "AIzaSyCjc_pVEDi4qsv5MtC2dMXzpIaDoRFLsxw", }, ClientType::AndroidScreenEmbed => { name: "ANDROID", @@ -143,9 +144,9 @@ module YoutubeAPI ClientType::IOSMusic => { name: "IOS_MUSIC", name_proto: "26", - version: "5.21", + version: "6.42", api_key: "AIzaSyBAETezhkwP0ZWA02RsqT1zu78Fpt0bC_s", - user_agent: "com.google.ios.youtubemusic/5.21 (iPhone14,5; U; CPU iOS 15_6 like Mac OS X;)", + user_agent: "com.google.ios.youtubemusic/6.42 (iPhone14,5; U; CPU iOS 17_4 like Mac OS X;)", device_make: "Apple", device_model: "iPhone14,5", os_name: "iPhone", @@ -158,7 +159,7 @@ module YoutubeAPI ClientType::TvHtml5 => { name: "TVHTML5", name_proto: "7", - version: "7.20220325", + version: "7.20240304.10.00", api_key: DEFAULT_API_KEY, }, ClientType::TvHtml5ScreenEmbed => { -- cgit v1.2.3