feat: mutliple vertd instances support

This commit is contained in:
not-nullptr 2025-09-18 17:47:42 +01:00
parent ce88e01a22
commit d2a73e0840
8 changed files with 266 additions and 47 deletions

View File

@ -109,7 +109,7 @@
"unavailable": "unavailable (is the url right?)",
"description": "The <code>vertd</code> 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",

View File

@ -109,7 +109,7 @@
"unavailable": "no disponible (¿has comprobado la url?)",
"description": "<code>vertd</code> 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.",

View File

@ -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 <U extends keyof RouteMap>(
url: U,
options: RequestInit,
): Promise<RouteMap[U]> => {
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<RouteMap[U]> = null!;
@ -124,7 +125,7 @@ const progressEstimate = (
};
const uploadFile = async (file: VertFile): Promise<UploadResponse> => {
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<boolean> {
if (!Settings.instance.settings.vertdURL) {
if (!(await VertdInstance.instance.url())) {
return false;
}

33
src/lib/ip.ts Normal file
View File

@ -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<IpInfo> => {
return await fetch("https://ipapi.co/json/").then((r) => r.json());
};

View File

@ -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<string | null>(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"]()}
</p>
<p class="text-sm text-muted font-normal">
{@html link("vertd_link", m["settings.vertd.hosting_info"](), GITHUB_URL_VERTD)}
{@html link(
"vertd_link",
m["settings.vertd.hosting_info"](),
GITHUB_URL_VERTD,
)}
</p>
<div class="flex flex-col gap-2">
<p class="text-base font-bold">{m["settings.vertd.instance_url"]()}</p>
<input
type="text"
placeholder={m["settings.vertd.url_placeholder"]()}
bind:value={settings.vertdURL}
<p class="text-base font-bold">
{m["settings.vertd.instance"]()}
</p>
<Dropdown
options={[
m["settings.vertd.auto_instance"](),
m["settings.vertd.eu_instance"](),
m["settings.vertd.us_instance"](),
m["settings.vertd.custom_instance"](),
]}
onselect={(selected) => {
let inner: VertdInner;
switch (selected) {
case m["settings.vertd.auto_instance"]():
inner = { type: "auto" };
break;
case m["settings.vertd.eu_instance"]():
inner = { type: "eu" };
break;
case m["settings.vertd.us_instance"]():
inner = { type: "us" };
break;
case m["settings.vertd.custom_instance"]():
inner = {
type: "custom",
};
break;
default:
inner = { type: "auto" };
}
VertdInstance.instance.set(inner);
}}
selected={(() => {
switch (VertdInstance.instance.innerData().type) {
case "auto":
return m["settings.vertd.auto_instance"]();
case "eu":
return m["settings.vertd.eu_instance"]();
case "us":
return m["settings.vertd.us_instance"]();
case "custom":
return m[
"settings.vertd.custom_instance"
]();
}
})()}
settingsStyle
/>
{#if VertdInstance.instance.innerData().type === "custom"}
<input
type="text"
placeholder={m["settings.vertd.url_placeholder"]()}
bind:value={settings.vertdURL}
/>
{/if}
</div>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<p class="text-base font-bold">{m["settings.vertd.conversion_speed"]()}</p>
<p class="text-base font-bold">
{m["settings.vertd.conversion_speed"]()}
</p>
<p class="text-sm text-muted font-normal">
{m["settings.vertd.speed_description"]()}
</p>
@ -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) => {

View File

@ -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);

View File

@ -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<IpInfo | null>(null);
private inner = $state<VertdInner>({
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;
}
}

View File

@ -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(() => {