From d2a73e084000652f18f24ee01fdc3a93cab2d5d6 Mon Sep 17 00:00:00 2001
From: not-nullptr
Date: Thu, 18 Sep 2025 17:47:42 +0100
Subject: [PATCH] feat: mutliple vertd instances support
---
messages/en.json | 8 +-
messages/es.json | 2 +-
src/lib/converters/vertd.svelte.ts | 10 +-
src/lib/ip.ts | 33 +++++
src/lib/sections/settings/Vertd.svelte | 127 +++++++++++++-----
src/lib/sections/settings/index.svelte.ts | 3 +
.../sections/settings/vertdSettings.svelte.ts | 120 +++++++++++++++++
src/routes/+layout.svelte | 10 +-
8 files changed, 266 insertions(+), 47 deletions(-)
create mode 100644 src/lib/ip.ts
create mode 100644 src/lib/sections/settings/vertdSettings.svelte.ts
diff --git a/messages/en.json b/messages/en.json
index 2f34788..88266fd 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -109,7 +109,7 @@
"unavailable": "unavailable (is the url right?)",
"description": "The vertd project is a server wrapper for FFmpeg. This allows you to convert videos through the convenience of VERT's web interface, while still being able to harness the power of your GPU to do it as quickly as possible.",
"hosting_info": "We host a public instance for your convenience, but it is quite easy to host your own on your PC or server if you know what you are doing. You can download the server binaries [vertd_link]here[/vertd_link] - the process of setting this up will become easier in the future, so stay tuned!",
- "instance_url": "Instance URL",
+ "instance": "Instance",
"url_placeholder": "Example: http://localhost:24153",
"conversion_speed": "Conversion speed",
"speed_description": "This describes the tradeoff between speed and quality. Faster speeds will result in lower quality, but will get the job done quicker.",
@@ -120,7 +120,11 @@
"medium": "Medium",
"fast": "Fast",
"ultra_fast": "Ultra Fast"
- }
+ },
+ "auto_instance": "Auto (recommended)",
+ "eu_instance": "Falkenstein, Germany",
+ "us_instance": "Washington, USA",
+ "custom_instance": "Custom"
},
"privacy": {
"title": "Privacy",
diff --git a/messages/es.json b/messages/es.json
index a38a352..96b9fd0 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -109,7 +109,7 @@
"unavailable": "no disponible (¿has comprobado la url?)",
"description": "vertd es un proyecto que actúa como un servidor intermediario (\"wrapper\") para FFmpeg. Permite convertir vídeos sin dejar de lado la conveniente interfaz web de VERT y, a la vez, aprovecha la potencia de tu GPU para hacerlo lo más rápido posible.",
"hosting_info": "Alojamos una instancia pública para tu conveniencia, pero es bastante fácil alojar una propia en tu PC o servidor si sabes lo que estás haciendo. Puedes descargar los binarios del servidor [vertd_link]aquí[/vertd_link]. ¡El proceso de instalación será más fácil en el futuro, así que mantente atento!",
- "instance_url": "URL de la instancia",
+ "instance": "Instancia",
"url_placeholder": "Ejemplo: http://localhost:24153",
"conversion_speed": "Velocidad de conversión",
"speed_description": "Esto describe el equilibrio entre velocidad y calidad. Velocidades más rápidas resultarán en una calidad más baja, pero harán el trabajo más rápido.",
diff --git a/src/lib/converters/vertd.svelte.ts b/src/lib/converters/vertd.svelte.ts
index 1ea3fe2..60f806d 100644
--- a/src/lib/converters/vertd.svelte.ts
+++ b/src/lib/converters/vertd.svelte.ts
@@ -1,5 +1,6 @@
import { log } from "$lib/logger";
import { Settings } from "$lib/sections/settings/index.svelte";
+import { VertdInstance } from "$lib/sections/settings/vertdSettings.svelte";
import { VertFile } from "$lib/types";
import { Converter, FormatInfo } from "./converter.svelte";
@@ -33,7 +34,7 @@ const vertdFetch = async (
url: U,
options: RequestInit,
): Promise => {
- const domain = Settings.instance.settings.vertdURL;
+ const domain = await VertdInstance.instance.url();
const res = await fetch(`${domain}${url}`, options);
const text = await res.text();
let json: VertdResponse = null!;
@@ -124,7 +125,7 @@ const progressEstimate = (
};
const uploadFile = async (file: VertFile): Promise => {
- const apiUrl = Settings.instance.settings.vertdURL;
+ const apiUrl = await VertdInstance.instance.url();
const formData = new FormData();
formData.append("file", file.file, file.name);
const xhr = new XMLHttpRequest();
@@ -244,10 +245,9 @@ export class VertdConverter extends Converter {
if (to.startsWith(".")) to = to.slice(1);
const uploadRes = await uploadFile(input);
- console.log(uploadRes);
+ const apiUrl = await VertdInstance.instance.url();
return new Promise((resolve, reject) => {
- const apiUrl = Settings.instance.settings.vertdURL;
const protocol = apiUrl.startsWith("https") ? "wss:" : "ws:";
const ws = new WebSocket(
`${protocol}//${apiUrl.replace("http://", "").replace("https://", "")}/api/ws`,
@@ -304,7 +304,7 @@ export class VertdConverter extends Converter {
}
public async valid(): Promise {
- if (!Settings.instance.settings.vertdURL) {
+ if (!(await VertdInstance.instance.url())) {
return false;
}
diff --git a/src/lib/ip.ts b/src/lib/ip.ts
new file mode 100644
index 0000000..7c37105
--- /dev/null
+++ b/src/lib/ip.ts
@@ -0,0 +1,33 @@
+export interface IpInfo {
+ ip: string;
+ network: string;
+ version: string;
+ city: string;
+ region: string;
+ region_code: string;
+ country: string;
+ country_name: string;
+ country_code: string;
+ country_code_iso3: string;
+ country_capital: string;
+ country_tld: string;
+ continent_code: string;
+ in_eu: boolean;
+ postal: string;
+ latitude: number;
+ longitude: number;
+ timezone: string;
+ utc_offset: string;
+ country_calling_code: string;
+ currency: string;
+ currency_name: string;
+ languages: string;
+ country_area: number;
+ country_population: number;
+ asn: string;
+ org: string;
+}
+
+export const ip = async (): Promise => {
+ return await fetch("https://ipapi.co/json/").then((r) => r.json());
+};
diff --git a/src/lib/sections/settings/Vertd.svelte b/src/lib/sections/settings/Vertd.svelte
index d19546b..476a67a 100644
--- a/src/lib/sections/settings/Vertd.svelte
+++ b/src/lib/sections/settings/Vertd.svelte
@@ -8,6 +8,7 @@
import { vertdLoaded } from "$lib/store/index.svelte";
import { m } from "$lib/paraglide/messages";
import { link } from "$lib/store/index.svelte";
+ import { VertdInstance, type VertdInner } from "./vertdSettings.svelte";
let vertdCommit = $state(null);
let abortController: AbortController | null = null;
@@ -15,33 +16,29 @@
const { settings }: { settings: ISettings } = $props();
$effect(() => {
- if (settings.vertdURL) {
- if (abortController) abortController.abort();
- abortController = new AbortController();
- const { signal } = abortController;
+ if (abortController) abortController.abort();
+ abortController = new AbortController();
+ const { signal } = abortController;
- vertdCommit = "loading";
- fetch(`${settings.vertdURL}/api/version`, { signal })
- .then((res) => {
- if (!res.ok) throw new Error("bad response");
+ vertdCommit = "loading";
+ VertdInstance.instance
+ .url()
+ .then((u) => fetch(`${u}/api/version`, { signal }))
+ .then((res) => {
+ if (!res.ok) throw new Error("bad response");
+ vertdLoaded.set(false);
+ return res.json();
+ })
+ .then((data) => {
+ vertdCommit = data.data;
+ vertdLoaded.set(true);
+ })
+ .catch((err) => {
+ if (err.name !== "AbortError") {
+ vertdCommit = null;
vertdLoaded.set(false);
- return res.json();
- })
- .then((data) => {
- vertdCommit = data.data;
- vertdLoaded.set(true);
- })
- .catch((err) => {
- if (err.name !== "AbortError") {
- vertdCommit = null;
- vertdLoaded.set(false);
- }
- });
- } else {
- if (abortController) abortController.abort();
- vertdCommit = null;
- vertdLoaded.set(false);
- }
+ }
+ });
return () => {
if (abortController) abortController.abort();
@@ -66,7 +63,8 @@
"!text-muted": vertdCommit === "loading",
})}
>
- {m["settings.vertd.status"]()} {vertdCommit
+ {m["settings.vertd.status"]()}
+ {vertdCommit
? vertdCommit === "loading"
? m["settings.vertd.loading"]()
: m["settings.vertd.available"]({ commitId: vertdCommit })
@@ -78,19 +76,74 @@
{@html m["settings.vertd.description"]()}
- {@html link("vertd_link", m["settings.vertd.hosting_info"](), GITHUB_URL_VERTD)}
+ {@html link(
+ "vertd_link",
+ m["settings.vertd.hosting_info"](),
+ GITHUB_URL_VERTD,
+ )}
-
{m["settings.vertd.conversion_speed"]()}
+
+ {m["settings.vertd.conversion_speed"]()}
+
{m["settings.vertd.speed_description"]()}
@@ -108,7 +161,9 @@
selected={(() => {
switch (settings.vertdSpeed) {
case "verySlow":
- return m["settings.vertd.speeds.very_slow"]();
+ return m[
+ "settings.vertd.speeds.very_slow"
+ ]();
case "slower":
return m["settings.vertd.speeds.slower"]();
case "slow":
@@ -118,7 +173,9 @@
case "fast":
return m["settings.vertd.speeds.fast"]();
case "ultraFast":
- return m["settings.vertd.speeds.ultra_fast"]();
+ return m[
+ "settings.vertd.speeds.ultra_fast"
+ ]();
}
})()}
onselect={(selected) => {
diff --git a/src/lib/sections/settings/index.svelte.ts b/src/lib/sections/settings/index.svelte.ts
index f00f43d..e71ba38 100644
--- a/src/lib/sections/settings/index.svelte.ts
+++ b/src/lib/sections/settings/index.svelte.ts
@@ -1,6 +1,7 @@
import { PUB_VERTD_URL } from "$env/static/public";
import type { ConversionBitrate } from "$lib/converters/ffmpeg.svelte";
import type { ConversionSpeed } from "$lib/converters/vertd.svelte";
+import { VertdInstance } from "./vertdSettings.svelte";
export { default as Appearance } from "./Appearance.svelte";
export { default as Conversion } from "./Conversion.svelte";
@@ -34,9 +35,11 @@ export class Settings {
public save() {
localStorage.setItem("settings", JSON.stringify(this.settings));
+ VertdInstance.instance.save();
}
public load() {
+ VertdInstance.instance.load();
const ls = localStorage.getItem("settings");
if (!ls) return;
const settings: ISettings = JSON.parse(ls);
diff --git a/src/lib/sections/settings/vertdSettings.svelte.ts b/src/lib/sections/settings/vertdSettings.svelte.ts
new file mode 100644
index 0000000..4ecf8cf
--- /dev/null
+++ b/src/lib/sections/settings/vertdSettings.svelte.ts
@@ -0,0 +1,120 @@
+import { ip, type IpInfo } from "$lib/ip";
+import { Settings } from "./index.svelte";
+
+const LOCATIONS = [
+ {
+ latitude: 49.0976,
+ longitude: 12.4869,
+ url: "https://eu.vertd.vert.sh",
+ },
+ {
+ latitude: 47.6587,
+ longitude: -117.426,
+ url: "https://usa.vertd.vert.sh",
+ },
+];
+
+const toRad = (value: number) => (value * Math.PI) / 180;
+const haversine = (lat1: number, lon1: number, lat2: number, lon2: number) => {
+ const R = 6371; // km
+ const dLat = toRad(lat2 - lat1);
+ const dLon = toRad(lon2 - lon1);
+ const a =
+ Math.sin(dLat / 2) * Math.sin(dLat / 2) +
+ Math.cos(toRad(lat1)) *
+ Math.cos(toRad(lat2)) *
+ Math.sin(dLon / 2) *
+ Math.sin(dLon / 2);
+ const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+ const d = R * c;
+ return d;
+};
+
+export type VertdInner =
+ | { type: "auto" }
+ | { type: "eu" }
+ | { type: "us" }
+ | { type: "custom" };
+
+export class VertdInstance {
+ public static instance = new VertdInstance();
+
+ private cachedIp = $state
(null);
+
+ private inner = $state({
+ type: "auto",
+ });
+
+ public save() {
+ localStorage.setItem("vertdInstance", JSON.stringify(this.inner));
+ }
+
+ public load() {
+ const ls = localStorage.getItem("vertdInstance");
+ if (!ls) return;
+ const inner: VertdInner = JSON.parse(ls);
+ this.inner = {
+ ...this.inner,
+ ...inner,
+ };
+ }
+
+ public innerData() {
+ return this.inner;
+ }
+
+ public set(inner: VertdInner) {
+ this.inner = inner;
+ this.save();
+ }
+
+ public async url() {
+ switch (this.inner.type) {
+ case "auto": {
+ if (!this.cachedIp) {
+ this.cachedIp = await ip();
+ }
+
+ return this.geographicallyOptimalInstance(this.cachedIp);
+ }
+
+ case "eu": {
+ return "https://eu.vertd.vert.sh";
+ }
+
+ case "us": {
+ return "https://usa.vertd.vert.sh";
+ }
+
+ case "custom": {
+ return Settings.instance.settings.vertdURL;
+ }
+ }
+ }
+
+ private geographicallyOptimalInstance(ip: IpInfo) {
+ let bestLocation = LOCATIONS[0];
+ let bestDistance = haversine(
+ ip.latitude,
+ ip.longitude,
+ bestLocation.latitude,
+ bestLocation.longitude,
+ );
+
+ for (let i = 1; i < LOCATIONS.length; i++) {
+ const location = LOCATIONS[i];
+ const distance = haversine(
+ ip.latitude,
+ ip.longitude,
+ location.latitude,
+ location.longitude,
+ );
+ if (distance < bestDistance) {
+ bestDistance = distance;
+ bestLocation = location;
+ }
+ }
+
+ return bestLocation.url;
+ }
+}
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index 23913db..c883433 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -22,6 +22,7 @@
import { page } from "$app/state";
import { initStores as initAnimStores } from "$lib/animation/index.js";
import { locales, localizeHref } from "$lib/paraglide/runtime";
+ import { VertdInstance } from "$lib/sections/settings/vertdSettings.svelte.js";
let { children, data } = $props();
let enablePlausible = $state(false);
@@ -72,11 +73,12 @@
);
Settings.instance.load();
- fetch(`${Settings.instance.settings.vertdURL}/api/version`).then(
- (res) => {
+ VertdInstance.instance
+ .url()
+ .then((u) => fetch(`${u}/api/version`))
+ .then((res) => {
if (res.ok) $vertdLoaded = true;
- },
- );
+ });
});
$effect(() => {