Add companion blocked status

This commit is contained in:
weidenwiesel
2026-01-01 11:50:41 +01:00
parent d5eb239eec
commit 663c640779
5 changed files with 391 additions and 48 deletions
+19 -4
View File
@@ -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,6 +1044,10 @@ h1, h2, h3, h4, h5, p,
margin: 0 2px;
}
#download {
display:none;
}
#download_widget {
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 {
@@ -0,0 +1,290 @@
From 6953eed8bd7dfb68244947dbdd274a23ad501743 Mon Sep 17 00:00:00 2001
From: Fijxu <fijxu@nadeko.net>, weidenwiesel <webmaster@nerdvpn.de>
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<Worker, "postMessage"> {
postMessage(message: InputMessage): void;
@@ -61,6 +62,7 @@ export type TokenMinter = ReturnType<typeof createMinter>;
export const poTokenGenerate = (
config: Config,
metrics: Metrics | undefined,
+ companionStatus: CompanionStatus,
): Promise<{ innertubeClient: Innertube; tokenMinter: TokenMinter }> => {
const { promise, resolve, reject } = Promise.withResolvers<
Awaited<ReturnType<typeof poTokenGenerate>>
@@ -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
@@ -1,26 +1,26 @@
From 4a2ea38acf78b4239bb3b8dc53d9daae275d4d11 Mon Sep 17 00:00:00 2001
From 7d8502bea6019f9c46f0dd50c11bd3db7ee09586 Mon Sep 17 00:00:00 2001
From: weidenwiesel <webmaster@nerdvpn.de>
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.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/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(-)
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,12 +143,28 @@ 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
+ 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
+ @@check_mutex.synchronize do
@@ -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 @@
</div>
</div>
@@ -522,8 +552,9 @@ index df83aab2..bea5a939 100644
+ <%= HTML.escape(CONFIG.backend_name_prefix + (index + 1).to_s) %> <%= HTML.escape(companion.note) %>
+ <span style="font-size: 1rem; color:
+ <% if status[index] == BackendInfo::Status::Dead.to_i %> #fd4848; <% end %>
+ <% if status[index] == BackendInfo::Status::Blocked.to_i %> #ddc338; <% end %>
+ <% if status[index] == BackendInfo::Status::Working.to_i %> #42ae3c; <% end %>
+ "></span>
+ "></span>
+ </a>
+ <% if !(index == CONFIG.invidious_companion.size-1) %>
+ <span> | </span>
@@ -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
@@ -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) %>
- <span style="font-size: 1rem; color:
- <% if status[index] == BackendInfo::Status::Dead.to_i %> #fd4848; <% end %>
- <% if status[index] == BackendInfo::Status::Blocked.to_i %> #ddc338; <% end %>
- <% if status[index] == BackendInfo::Status::Working.to_i %> #42ae3c; <% end %>
- "></span>
- "></span>
- </a>
+ <% if status[index] == BackendInfo::Status::Working.to_i %>
+ <a class="backend backend-healthy backend_number_<%= index %><%= current_backend == index ? " backend-selected" : "" %>" href="/switchbackend?backend_id=<%= index.to_s %>&referer=<%= current_page %>">
+ <%= HTML.escape(CONFIG.backend_name_prefix + (index + 1).to_s) %> <%= HTML.escape(companion.note) %>
+ <span class="backend-status"></span>
+ <span class="backend-status"></span>
+ </a>
+ <% end %>
+ <% if status[index] == BackendInfo::Status::Blocked.to_i %>
+ <span class="backend backend-blocked backend_number_<%= index %><%= current_backend == index ? " backend-selected" : "" %>">
+ <%= HTML.escape(CONFIG.backend_name_prefix + (index + 1).to_s) %> <%= HTML.escape(companion.note) %>
+ <span class="backend-status">❚</span>
+ </span>
+ <% end %>
+ <% if status[index] == BackendInfo::Status::Dead.to_i %>
+ <span class="backend backend-unhealthy backend_number_<%= index %><%= current_backend == index ? " backend-selected" : "" %>">
+ <%= HTML.escape(CONFIG.backend_name_prefix + (index + 1).to_s) %> <%= HTML.escape(companion.note) %>
+ <span class="backend-status"></span>
+ <span class="backend-status"></span>
+ </span>
+ <% end %>
<% if !(index == CONFIG.invidious_companion.size-1) %>