From 663c640779abf4f692f8e5a74b370501c7761171 Mon Sep 17 00:00:00 2001 From: weidenwiesel Date: Thu, 1 Jan 2026 11:50:41 +0100 Subject: [PATCH] Add companion blocked status --- assets/css/default.css | 25 +- .../0004-store-companion-status.patch | 290 ++++++++++++++++++ .../0002-companion-backend-selector.patch | 107 ++++--- .../0013-backends-health-status-page.patch | 2 +- .../0014-disable-unhealthy-backends.patch | 15 +- 5 files changed, 391 insertions(+), 48 deletions(-) create mode 100644 patches/companion/0004-store-companion-status.patch diff --git a/assets/css/default.css b/assets/css/default.css index e80ec0a..3bd762c 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -149,7 +149,8 @@ body a.channel-owner { color: #81A1C1; margin-bottom: 5px; } -.backend-unhealthy { +.backend-unhealthy, +.backend-blocked { cursor: not-allowed; } .backend-status { @@ -159,6 +160,9 @@ body a.channel-owner { .backend-healthy .backend-status { color: #42ae3c; } +.backend-blocked .backend-status { + color: #ddc338; +} .backend-unhealthy .backend-status { color: #fd4848; } @@ -195,8 +199,9 @@ body a.channel-owner { text-align: center; } #generic-error-message { - font-size: 1.25em; + font-size: 1rem; margin: 20px 0; + color: var(--Color1); } #watch-error { margin: 40px 0; @@ -210,7 +215,8 @@ body a.channel-owner { #watch-error-message { margin: -20px 0 20px 0; - font-size: 0.9em; + font-size: 0.95em; + color: var(--Color1); } #next-steps { @@ -271,6 +277,10 @@ div { padding-right: 10px; } +.pure-form { + margin-bottom: 20px; +} + /* * Buttons @@ -403,6 +413,7 @@ div.thumbnail > .bottom-right-overlay { .searchbar .pure-form { display: flex; + margin-bottom: 0; } .searchbar .pure-form fieldset { @@ -1033,8 +1044,12 @@ h1, h2, h3, h4, h5, p, margin: 0 2px; } +#download { + display:none; +} + #download_widget { - width: 100%; + width: 100%; } @@ -1511,7 +1526,7 @@ body.light-theme { .light-theme .length, .light-theme p.length { color: #434C5E; - background-color: #ECEFF4; + background-color: #ECEFF4!important; } .light-theme p.channel-name { diff --git a/patches/companion/0004-store-companion-status.patch b/patches/companion/0004-store-companion-status.patch new file mode 100644 index 0000000..16f081b --- /dev/null +++ b/patches/companion/0004-store-companion-status.patch @@ -0,0 +1,290 @@ +From 6953eed8bd7dfb68244947dbdd274a23ad501743 Mon Sep 17 00:00:00 2001 +From: Fijxu , weidenwiesel +Date: Mon, 13 Oct 2025 10:26:32 +0200 +Subject: [PATCH 4/4] [Patch] 004 - store companion status + +--- + src/constants.ts | 3 +++ + src/lib/helpers/companionStatus.ts | 33 +++++++++++++++++++++++++ + src/lib/jobs/potoken.ts | 7 ++++++ + src/lib/types/HonoVariables.ts | 2 ++ + src/main.ts | 6 +++++ + src/routes/index.ts | 2 ++ + src/routes/status.ts | 26 +++++++++++++++++++ + src/routes/youtube_api_routes/player.ts | 25 ++++++++++++++++++- + 8 files changed, 103 insertions(+), 1 deletion(-) + create mode 100644 src/lib/helpers/companionStatus.ts + create mode 100644 src/routes/status.ts + +diff --git a/src/constants.ts b/src/constants.ts +index 973d1a1..5c14d06 100644 +--- a/src/constants.ts ++++ b/src/constants.ts +@@ -8,4 +8,6 @@ export const PLAYER_ID = undefined; + + // Error message shown when tokenMinter is not yet ready + export const TOKEN_MINTER_NOT_READY_MESSAGE = +- "Companion is starting. Please wait until a valid potoken is found. If this process takes too long, please consult: https://docs.invidious.io/youtube-errors-explained/#po-token-initialization-taking-too-much-time-to-complete"; ++ "This backend is still starting and not yet available. Please wait for a short time or use another backend."; ++export const COMPANION_BLOCKED_MESSAGE = ++ "This backend is blocked by Youtube. Please wait until it has refreshed itself or switch to another backend." +diff --git a/src/lib/jobs/potoken.ts b/src/lib/jobs/potoken.ts +index a2616da..718eedd 100644 +--- a/src/lib/jobs/potoken.ts ++++ b/src/lib/jobs/potoken.ts +@@ -20,6 +20,7 @@ const { getFetchClient } = await import(getFetchClientLocation); + + import { InputMessage, OutputMessageSchema } from "./worker.ts"; + import { PLAYER_ID } from "../../constants.ts"; ++import { CompanionStatus } from "../helpers/companionStatus.ts"; + + interface TokenGeneratorWorker extends Omit { + postMessage(message: InputMessage): void; +@@ -61,6 +62,7 @@ export type TokenMinter = ReturnType; + export const poTokenGenerate = ( + config: Config, + metrics: Metrics | undefined, ++ companionStatus: CompanionStatus, + ): Promise<{ innertubeClient: Innertube; tokenMinter: TokenMinter }> => { + const { promise, resolve, reject } = Promise.withResolvers< + Awaited> +@@ -111,6 +113,7 @@ export const poTokenGenerate = ( + config, + integrityTokenBasedMinter: minter, + metrics, ++ companionStatus, + }); + console.log("[INFO] Successfully generated PO token"); + const numberToKill = workers.length - 1; +@@ -118,11 +121,13 @@ export const poTokenGenerate = ( + const workerToKill = workers.shift(); + workerToKill?.terminate(); + } ++ companionStatus.setStatusOK(); + return resolve({ + innertubeClient: instantiatedInnertubeClient, + tokenMinter: minter, + }); + } catch (err) { ++ companionStatus.setStatusBlocked(); + console.log("[WARN] Failed to get valid PO token, will retry", { + err, + }); +@@ -140,11 +145,13 @@ async function checkToken({ + config, + integrityTokenBasedMinter, + metrics, ++ companionStatus, + }: { + instantiatedInnertubeClient: Innertube; + config: Config; + integrityTokenBasedMinter: TokenMinter; + metrics: Metrics | undefined; ++ companionStatus: CompanionStatus; + }) { + const fetchImpl = getFetchClient(config); + +diff --git a/src/lib/types/HonoVariables.ts b/src/lib/types/HonoVariables.ts +index f1e64d5..8b5c6a1 100644 +--- a/src/lib/types/HonoVariables.ts ++++ b/src/lib/types/HonoVariables.ts +@@ -2,10 +2,12 @@ import { Innertube } from "youtubei.js"; + import type { TokenMinter } from "../jobs/potoken.ts"; + import type { Config } from "../helpers/config.ts"; + import { Metrics } from "../helpers/metrics.ts"; ++import { CompanionStatus } from "../helpers/companionStatus.ts"; + + export type HonoVariables = { + innertubeClient: Innertube; + config: Config; + tokenMinter: TokenMinter | undefined; + metrics: Metrics | undefined; ++ companionStatus: CompanionStatus; + }; +diff --git a/src/main.ts b/src/main.ts +index 7bbe819..5dee6b6 100644 +--- a/src/main.ts ++++ b/src/main.ts +@@ -13,6 +13,7 @@ const config = await parseConfig(); + import { Metrics } from "./lib/helpers/metrics.ts"; + import { PLAYER_ID } from "./constants.ts"; + import { jsInterpreter } from "./lib/helpers/jsInterpreter.ts"; ++import { CompanionStatus } from "./lib/helpers/companionStatus.ts"; + + const args = parseArgs(Deno.args); + +@@ -46,6 +47,7 @@ const companionApp = new Hono({ + getPath: (req) => new URL(req.url).pathname, + }).basePath(config.server.base_path); + const metrics = config.server.enable_metrics ? new Metrics() : undefined; ++const companionStatus = new CompanionStatus(); + + let tokenMinter: TokenMinter | undefined; + let innertubeClient: Innertube; +@@ -91,6 +93,7 @@ if (!innertubeClientOauthEnabled) { + poTokenGenerate, + config, + metrics, ++ companionStatus, + ), + { minTimeout: 1_000, maxTimeout: 60_000, multiplier: 5, jitter: 0 }, + ).then((result) => { +@@ -116,6 +119,7 @@ if (!innertubeClientOauthEnabled) { + ({ innertubeClient, tokenMinter } = await poTokenGenerate( + config, + metrics, ++ companionStatus, + )); + } catch (err) { + metrics?.potokenGenerationFailure.inc(); +@@ -162,6 +166,7 @@ companionApp.use("*", async (c, next) => { + c.set("tokenMinter", tokenMinter); + c.set("config", config); + c.set("metrics", metrics); ++ c.set("companionStatus", companionStatus); + await next(); + }); + companionRoutes(companionApp, config); +diff --git a/src/routes/index.ts b/src/routes/index.ts +index 09cd4ff..5113811 100644 +--- a/src/routes/index.ts ++++ b/src/routes/index.ts +@@ -11,6 +11,7 @@ import videoPlaybackProxy from "./videoPlaybackProxy.ts"; + import type { Config } from "../lib/helpers/config.ts"; + import metrics from "./metrics.ts"; + import health from "./health.ts"; ++import status from "./status.ts"; + + export const customLogger = () => { + return; +@@ -53,6 +54,7 @@ export const miscRoutes = ( + config: Config, + ) => { + app.route("/healthz", health); ++ app.route("/status", status); + if (config.server.enable_metrics) { + app.route("/metrics", metrics); + } +diff --git a/src/routes/youtube_api_routes/player.ts b/src/routes/youtube_api_routes/player.ts +index f416fa6..c8d5d0d 100644 +--- a/src/routes/youtube_api_routes/player.ts ++++ b/src/routes/youtube_api_routes/player.ts +@@ -2,7 +2,10 @@ import { Hono } from "hono"; + import { youtubePlayerParsing } from "../../lib/helpers/youtubePlayerHandling.ts"; + import { HTTPException } from "hono/http-exception"; + import { validateVideoId } from "../../lib/helpers/validateVideoId.ts"; +-import { TOKEN_MINTER_NOT_READY_MESSAGE } from "../../constants.ts"; ++import { ++ COMPANION_BLOCKED_MESSAGE, ++ TOKEN_MINTER_NOT_READY_MESSAGE, ++} from "../../constants.ts"; + + const player = new Hono(); + +@@ -12,6 +15,7 @@ player.post("/player", async (c) => { + const config = c.get("config"); + const metrics = c.get("metrics"); + const tokenMinter = c.get("tokenMinter"); ++ const companionStatus = c.get("companionStatus"); + + // Check if tokenMinter is ready (only needed when PO token is enabled) + if (config.jobs.youtube_session.po_token_enabled && !tokenMinter) { +@@ -33,6 +37,25 @@ player.post("/player", async (c) => { + }); + } + ++ if (companionStatus.isBlocked()) { ++ return c.json({ ++ playabilityStatus: { ++ status: "ERROR", ++ reason: COMPANION_BLOCKED_MESSAGE, ++ errorScreen: { ++ playerErrorMessageRenderer: { ++ reason: { ++ simpleText: COMPANION_BLOCKED_MESSAGE, ++ }, ++ subreason: { ++ simpleText: COMPANION_BLOCKED_MESSAGE, ++ }, ++ }, ++ }, ++ }, ++ }); ++ } ++ + if (jsonReq.videoId) { + if (!validateVideoId(jsonReq.videoId)) { + throw new HTTPException(400, { +diff --git a/src/lib/helpers/companionStatus.ts b/src/lib/helpers/companionStatus.ts +new file mode 100644 +index 0000000..cf01fe5 +--- /dev/null ++++ b/src/lib/helpers/companionStatus.ts +@@ -0,0 +1,33 @@ ++enum Status { ++ Blocked, ++ OK, ++} ++ ++export class CompanionStatus { ++ private currentStatus: Status = Status.OK; ++ private _blockedCount: number = 0; ++ ++ public getBlockedCount(): number { ++ return this._blockedCount; ++ } ++ ++ public isBlocked(): boolean { ++ if (this.currentStatus == Status.Blocked) { ++ return true; ++ } else { ++ return false; ++ } ++ } ++ ++ public setStatusOK(): void { ++ console.log("[INFO] status set to OK"); ++ this._blockedCount = 0; ++ this.currentStatus = Status.OK; ++ } ++ ++ public setStatusBlocked(): void { ++ console.log("[INFO] status set to BLOCKED"); ++ this._blockedCount++; ++ this.currentStatus = Status.Blocked; ++ } ++} +diff --git a/src/routes/status.ts b/src/routes/status.ts +new file mode 100644 +index 0000000..61149b9 +--- /dev/null ++++ b/src/routes/status.ts +@@ -0,0 +1,26 @@ ++import { Hono } from "hono"; ++ ++const status = new Hono(); ++ ++interface Data { ++ blocked: boolean; ++ blockedCount: number; ++} ++ ++status.get("/", (c) => { ++ const companionStatus = c.get("companionStatus"); ++ ++ const data: Data = { ++ blocked: companionStatus.isBlocked(), ++ blockedCount: companionStatus.getBlockedCount(), ++ }; ++ ++ const responseBody = JSON.stringify(data); ++ ++ return new Response(responseBody, { ++ status: 200, ++ headers: { "Content-Type": "application/json" }, ++ }); ++}); ++ ++export default status; +-- +2.52.0 diff --git a/patches/invidious/0002-companion-backend-selector.patch b/patches/invidious/0002-companion-backend-selector.patch index bb03e3c..fd7a6b1 100644 --- a/patches/invidious/0002-companion-backend-selector.patch +++ b/patches/invidious/0002-companion-backend-selector.patch @@ -1,26 +1,26 @@ -From 4a2ea38acf78b4239bb3b8dc53d9daae275d4d11 Mon Sep 17 00:00:00 2001 +From 7d8502bea6019f9c46f0dd50c11bd3db7ee09586 Mon Sep 17 00:00:00 2001 From: weidenwiesel -Date: Sat, 20 Dec 2025 11:26:58 +0100 +Date: Thu, 1 Jan 2026 11:04:43 +0100 Subject: [PATCH 02/16] Companion backend selector --- - locales/en-US.json | 3 +- - src/invidious.cr | 14 +++- - src/invidious/config.cr | 8 ++ - src/invidious/helpers/backend_info.cr | 90 +++++++++++++++++++++ - src/invidious/jobs/backend_checker.cr | 14 ++++ - src/invidious/routes/backend_switcher.cr | 16 ++++ - src/invidious/routes/before_all.cr | 57 ++++++++++++- - src/invidious/routes/companion.cr | 12 ++- - src/invidious/routes/watch.cr | 2 +- - src/invidious/routing.cr | 1 + - src/invidious/user/cookies.cr | 21 +++++ - src/invidious/videos.cr | 10 +-- - src/invidious/videos/parser.cr | 8 +- - src/invidious/views/template.ecr | 31 +++++++ - src/invidious/yt_backend/connection_pool.cr | 15 ++-- - src/invidious/yt_backend/youtube_api.cr | 15 +++- - 16 files changed, 285 insertions(+), 32 deletions(-) + locales/en-US.json | 3 +- + src/invidious.cr | 14 ++- + src/invidious/config.cr | 9 ++ + src/invidious/helpers/backend_info.cr | 114 ++++++++++++++++++++ + src/invidious/jobs/backend_checker.cr | 14 +++ + src/invidious/routes/backend_switcher.cr | 16 +++ + src/invidious/routes/before_all.cr | 61 ++++++++++- + src/invidious/routes/companion.cr | 12 ++- + src/invidious/routes/watch.cr | 2 +- + src/invidious/routing.cr | 1 + + src/invidious/user/cookies.cr | 21 ++++ + src/invidious/videos.cr | 10 +- + src/invidious/videos/parser.cr | 8 +- + src/invidious/views/template.ecr | 32 ++++++ + src/invidious/yt_backend/connection_pool.cr | 15 ++- + src/invidious/yt_backend/youtube_api.cr | 15 ++- + 16 files changed, 315 insertions(+), 32 deletions(-) create mode 100644 src/invidious/helpers/backend_info.cr create mode 100644 src/invidious/jobs/backend_checker.cr create mode 100644 src/invidious/routes/backend_switcher.cr @@ -70,7 +70,7 @@ index 52fbca27..8e0d2c1f 100644 def popular_videos diff --git a/src/invidious/config.cr b/src/invidious/config.cr -index 7853d9a3..f3321aff 100644 +index 7853d9a3..fe180ec3 100644 --- a/src/invidious/config.cr +++ b/src/invidious/config.cr @@ -87,6 +87,8 @@ class Config @@ -82,7 +82,7 @@ index 7853d9a3..f3321aff 100644 end # Number of threads to use for crawling videos from channels (for updating subscriptions) -@@ -180,6 +182,12 @@ class Config +@@ -180,6 +182,13 @@ class Config # Playlist length limit property playlist_length_limit : Int32 = 500 @@ -90,6 +90,7 @@ index 7853d9a3..f3321aff 100644 + + property server_id_cookie_name : String = "COMPANION_ID" + property check_backends_interval : Int32 = 30 ++ property check_backends_path : String = "/healthz" + property backend_name_prefix : String = "Backend" + def disabled?(option) @@ -97,16 +98,24 @@ index 7853d9a3..f3321aff 100644 when Bool diff --git a/src/invidious/helpers/backend_info.cr b/src/invidious/helpers/backend_info.cr new file mode 100644 -index 00000000..f576245e +index 00000000..e675fab6 --- /dev/null +++ b/src/invidious/helpers/backend_info.cr -@@ -0,0 +1,90 @@ +@@ -0,0 +1,114 @@ +module BackendInfo + extend self + + enum Status + Dead = 0 -+ Working = 1 ++ Blocked = 1 ++ Working = 2 ++ end ++ ++ struct CompanionData ++ include JSON::Serializable ++ property blocked : Bool = false ++ @[JSON::Field(key: "blockedCount")] ++ property blocked_count : Int64 = 0 + end + + @@status : Array(Int32) = Array.new(CONFIG.invidious_companion.size, Status::Dead.to_i) @@ -134,11 +143,27 @@ index 00000000..f576245e + begin + client = HTTP::Client.new(companion.private_url) + client.connect_timeout = 10.seconds -+ response = client.get("/healthz") ++ response = client.get(CONFIG.check_backends_path) + if response.status_code == 200 -+ @@check_mutex.synchronize do -+ updated_status[index] = Status::Working.to_i -+ updated_ends.push(index) ++ if response.content_type == "application/json" ++ body = response.body ++ status_json = CompanionData.from_json(body) ++ if status_json.blocked ++ @@check_mutex.synchronize do ++ updated_status[index] = Status::Blocked.to_i ++ updated_ends.push(index) ++ end ++ else ++ @@check_mutex.synchronize do ++ updated_status[index] = Status::Working.to_i ++ updated_ends.push(index) ++ end ++ end ++ else ++ @@check_mutex.synchronize do ++ updated_status[index] = Status::Working.to_i ++ updated_ends.push(index) ++ end + end + generate_csp([companion.public_url.to_s], index) + else @@ -234,7 +259,7 @@ index 00000000..e4702729 + end +end diff --git a/src/invidious/routes/before_all.cr b/src/invidious/routes/before_all.cr -index 63b935ec..b34a2848 100644 +index 63b935ec..d88a87e0 100644 --- a/src/invidious/routes/before_all.cr +++ b/src/invidious/routes/before_all.cr @@ -1,6 +1,9 @@ @@ -247,7 +272,7 @@ index 63b935ec..b34a2848 100644 begin if prefs_cookie = env.request.cookies["PREFS"]? -@@ -20,6 +23,54 @@ module Invidious::Routes::BeforeAll +@@ -20,6 +23,58 @@ module Invidious::Routes::BeforeAll env.response.headers["X-XSS-Protection"] = "1; mode=block" env.response.headers["X-Content-Type-Options"] = "nosniff" @@ -277,8 +302,12 @@ index 63b935ec..b34a2848 100644 + end + end + -+ if current_companion > CONFIG.invidious_companion.size -+ current_companion = current_companion % CONFIG.invidious_companion.size - 1 ++ if current_companion < 0 ++ current_companion = rand(CONFIG.invidious_companion.size) ++ end ++ ++ if current_companion >= CONFIG.invidious_companion.size ++ current_companion = current_companion % CONFIG.invidious_companion.size + env.response.cookies[CONFIG.server_id_cookie_name] = Invidious::User::Cookies.server_id(host, current_companion) + end + @@ -302,7 +331,7 @@ index 63b935ec..b34a2848 100644 # Only allow the pages at /embed/* to be embedded if env.request.resource.starts_with?("/embed") frame_ancestors = "'self' file: http: https:" -@@ -33,11 +84,11 @@ module Invidious::Routes::BeforeAll +@@ -33,11 +88,11 @@ module Invidious::Routes::BeforeAll "default-src 'none'", "script-src 'self'", "style-src 'self' 'unsafe-inline'", @@ -318,7 +347,7 @@ index 63b935ec..b34a2848 100644 "frame-src 'self'", "frame-ancestors " + frame_ancestors, diff --git a/src/invidious/routes/companion.cr b/src/invidious/routes/companion.cr -index 949b213f..c435281a 100644 +index 811393ab..6ad1850e 100644 --- a/src/invidious/routes/companion.cr +++ b/src/invidious/routes/companion.cr @@ -1,13 +1,15 @@ @@ -355,7 +384,8 @@ index 949b213f..c435281a 100644 wrapper.client.post(url, env.request.headers, env.request.body) do |resp| return self.proxy_companion(env, resp) end -@@ -35,12 +39,14 @@ module Invidious::Routes::Companion +@@ -34,13 +38,15 @@ module Invidious::Routes::Companion + end def self.options_companion(env) + current_companion = env.get("current_companion").as(Int32) @@ -500,10 +530,10 @@ index 8114ad68..e263842f 100644 playability_status = response["playabilityStatus"]["status"] LOGGER.debug("try_fetch_streaming_data: [#{id}] Got playabilityStatus == #{playability_status}.") diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr -index df83aab2..bea5a939 100644 +index df83aab2..d4d96855 100644 --- a/src/invidious/views/template.ecr +++ b/src/invidious/views/template.ecr -@@ -105,6 +105,37 @@ +@@ -105,6 +105,38 @@ @@ -522,8 +552,9 @@ index df83aab2..bea5a939 100644 + <%= HTML.escape(CONFIG.backend_name_prefix + (index + 1).to_s) %> <%= HTML.escape(companion.note) %> + ++ ">❚ + + <% if !(index == CONFIG.invidious_companion.size-1) %> + | diff --git a/patches/invidious/0013-backends-health-status-page.patch b/patches/invidious/0013-backends-health-status-page.patch index f9c037a..33fa5d3 100644 --- a/patches/invidious/0013-backends-health-status-page.patch +++ b/patches/invidious/0013-backends-health-status-page.patch @@ -14,7 +14,7 @@ index dfd66f16..c027317c 100644 --- a/src/invidious/config.cr +++ b/src/invidious/config.cr @@ -192,6 +192,10 @@ class Config - property check_backends_interval : Int32 = 30 + property check_backends_path : String = "/healthz" property backend_name_prefix : String = "Backend" + # Weidenwiesel companion modifications diff --git a/patches/invidious/0014-disable-unhealthy-backends.patch b/patches/invidious/0014-disable-unhealthy-backends.patch index b425145..e63733d 100644 --- a/patches/invidious/0014-disable-unhealthy-backends.patch +++ b/patches/invidious/0014-disable-unhealthy-backends.patch @@ -11,7 +11,7 @@ diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr index 13b2fa51..7ee85fcc 100644 --- a/src/invidious/views/template.ecr +++ b/src/invidious/views/template.ecr -@@ -121,15 +121,20 @@ +@@ -121,16 +121,26 @@ <% end %> <% current_page = env.get("current_page") %> <% CONFIG.invidious_companion.each_with_index do | companion, index | %> @@ -19,19 +19,26 @@ index 13b2fa51..7ee85fcc 100644 - <%= HTML.escape(CONFIG.backend_name_prefix + (index + 1).to_s) %> <%= HTML.escape(companion.note) %> - +- ">❚ - + <% if status[index] == BackendInfo::Status::Working.to_i %> + " href="/switchbackend?backend_id=<%= index.to_s %>&referer=<%= current_page %>"> + <%= HTML.escape(CONFIG.backend_name_prefix + (index + 1).to_s) %> <%= HTML.escape(companion.note) %> -+ ++ + + <% end %> ++ <% if status[index] == BackendInfo::Status::Blocked.to_i %> ++ "> ++ <%= HTML.escape(CONFIG.backend_name_prefix + (index + 1).to_s) %> <%= HTML.escape(companion.note) %> ++ ++ ++ <% end %> + <% if status[index] == BackendInfo::Status::Dead.to_i %> + "> + <%= HTML.escape(CONFIG.backend_name_prefix + (index + 1).to_s) %> <%= HTML.escape(companion.note) %> -+ ++ + + <% end %> <% if !(index == CONFIG.invidious_companion.size-1) %>