fix: code review suggestions

yes used ai for this, all seems valid and noted though
This commit is contained in:
Maya 2026-03-20 13:55:47 +03:00
parent 8729584614
commit d3aeb9b696
14 changed files with 382 additions and 258 deletions

View File

@ -11,49 +11,49 @@
"@fontsource/lexend": "^5.2.11",
"@fontsource/radio-canada-big": "^5.2.7",
"@imagemagick/magick-wasm": "^0.0.37",
"@mediabunny/ac3": "^1.35.1",
"@mediabunny/flac-encoder": "^1.37.0",
"@mediabunny/mp3-encoder": "^1.35.1",
"@stripe/stripe-js": "^8.7.0",
"@mediabunny/ac3": "^1.40.0",
"@mediabunny/flac-encoder": "^1.40.0",
"@mediabunny/mp3-encoder": "^1.40.0",
"@stripe/stripe-js": "^8.11.0",
"byte-data": "^19.0.1",
"client-zip": "^2.5.0",
"clsx": "^2.1.1",
"fflate": "^0.8.2",
"lucide-svelte": "^0.554.0",
"mediabunny": "^1.37.0",
"music-metadata": "^11.12.0",
"mediabunny": "^1.40.0",
"music-metadata": "^11.12.3",
"overlayscrollbars": "^2.14.0",
"overlayscrollbars-svelte": "^0.5.5",
"p-queue": "^9.1.0",
"riff-file": "^1.0.3",
"sanitize-html": "^2.17.0",
"sanitize-html": "^2.17.2",
"svelte-stripe": "^1.4.0",
"vert-wasm": "^0.0.2",
"vite-plugin-wasm": "^3.5.0",
"vite-plugin-wasm": "^3.6.0",
},
"devDependencies": {
"@inlang/paraglide-js": "^2.11.0",
"@inlang/paraglide-js": "^2.15.0",
"@poppanator/sveltekit-svg": "^5.0.1",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.52.0",
"@sveltejs/kit": "^2.55.0",
"@sveltejs/vite-plugin-svelte": "^4.0.4",
"@types/eslint": "^9.6.1",
"@types/sanitize-html": "^2.16.0",
"autoprefixer": "^10.4.24",
"@types/sanitize-html": "^2.16.1",
"autoprefixer": "^10.4.27",
"css-select": "5.1.0",
"eslint": "^9.39.2",
"eslint": "^9.39.4",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^2.46.1",
"globals": "^15.15.0",
"prettier": "^3.8.1",
"prettier-plugin-svelte": "^3.4.1",
"prettier-plugin-svelte": "^3.5.1",
"prettier-plugin-tailwindcss": "^0.6.14",
"sass": "^1.97.3",
"svelte": "^5.51.2",
"svelte-check": "^4.4.0",
"sass": "^1.98.0",
"svelte": "^5.54.0",
"svelte-check": "^4.4.5",
"tailwindcss": "^3.4.19",
"typescript": "^5.9.3",
"typescript-eslint": "^8.55.0",
"typescript-eslint": "^8.57.1",
"vite": "^5.4.21",
"vite-plugin-top-level-await": "^1.6.0",
},

View File

@ -9,7 +9,7 @@
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"format": "prettier --write .",
"lint": "prettier --check . && eslint ."
"lint": "prettier --check .; p=$?; eslint .; e=$?; [ $p -eq 0 ] && [ $e -eq 0 ]"
},
"devDependencies": {
"@inlang/paraglide-js": "^2.15.0",

View File

@ -13,6 +13,25 @@
type Props = DialogProps<VertdErrorDetailsProps>;
let { additional }: Props = $props();
let errorBlobUrl = $state("");
$effect(() => {
if (!additional.errorMessage) {
errorBlobUrl = "";
return;
}
const nextUrl = URL.createObjectURL(
new Blob([additional.errorMessage], {
type: "text/plain",
}),
);
errorBlobUrl = nextUrl;
return () => {
URL.revokeObjectURL(nextUrl);
};
});
</script>
<div class="flex flex-col gap-2">
@ -41,13 +60,7 @@
{@html sanitize(link(
["view_link"],
m["convert.errors.vertd_details_error_message"](),
[
URL.createObjectURL(
new Blob([additional.errorMessage], {
type: "text/plain",
}),
),
],
[errorBlobUrl || "#"],
[true],
["text-blue-500 font-normal"],
))}

View File

@ -90,10 +90,11 @@ export class FFmpegConverter extends Converter {
this.error = (msg) => error(["converters", this.name], msg);
this.log(`created converter`);
if (!browser) return;
try {
// this is just to cache the wasm and js for when we actually use it. we're not using this ffmpeg instance
this.ffmpeg = new FFmpeg();
(async () => {
void (async () => {
try {
const baseURL =
"https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.10/dist/esm";
@ -105,7 +106,6 @@ export class FFmpegConverter extends Converter {
});
this.status = "ready";
})();
} catch (err) {
this.error(`Error loading ffmpeg: ${err}`);
this.status = "error";
@ -114,6 +114,7 @@ export class FFmpegConverter extends Converter {
message: m["workers.errors.ffmpeg"](),
});
}
})();
}
public async getAvailableSettings(): Promise<SettingDefinition[]> {
@ -251,6 +252,7 @@ export class FFmpegConverter extends Converter {
ffmpeg.on("log", errorListener);
try {
const buf = new Uint8Array(await input.file.arrayBuffer());
await ffmpeg.writeFile("input", buf);
this.log(`wrote ${input.name} to ffmpeg virtual fs`);
@ -266,31 +268,26 @@ export class FFmpegConverter extends Converter {
await ffmpeg.exec(command);
this.log("executed ffmpeg command");
if (conversionError) {
ffmpeg.off("log", errorListener);
ffmpeg.terminate();
throw new Error(conversionError);
}
if (conversionError) throw new Error(conversionError);
const output = (await ffmpeg.readFile(
"output" + to,
)) as unknown as Uint8Array;
if (!output || output.length === 0) {
ffmpeg.off("log", errorListener);
ffmpeg.terminate();
if (!output || output.length === 0)
throw new Error("empty file returned");
}
const outputFileName =
input.name.split(".").slice(0, -1).join(".") + to;
this.log(`read ${outputFileName} from ffmpeg virtual fs`);
ffmpeg.off("log", errorListener);
ffmpeg.terminate();
const outBuf = new Uint8Array(output).buffer.slice(0);
return new VertFile(new File([outBuf], outputFileName), to);
} finally {
ffmpeg.off("log", errorListener);
this.activeConversions.delete(input.id);
ffmpeg.terminate();
}
}
public async cancel(input: VertFile): Promise<void> {
@ -529,7 +526,7 @@ export class FFmpegConverter extends Converter {
// -map for each audio track
if (settings.tracks > 1) {
for (let i = 0; i < settings.tracks; i++) {
tracksArgs.push("-map", `0:a:${i - 1}`);
tracksArgs.push("-map", `0:a:${i}`);
}
} else {
tracksArgs = ["-map", "0:a:0"]; // default to first audio track if not specified

View File

@ -49,6 +49,7 @@ export class PandocConverter extends Converter {
this.activeConversions.set(file.id, worker);
try {
const loadMsg: WorkerMessage = {
type: "load",
wasm: this.wasm,
@ -71,8 +72,6 @@ export class PandocConverter extends Converter {
worker.postMessage(convertMsg);
const result = await waitForMessage(worker);
if (result.type === "error") {
worker.terminate();
// throw new Error(result.error);
const error = result.error.toString();
switch (result.errorKind) {
case "PandocUnknownReaderError": {
@ -106,12 +105,14 @@ export class PandocConverter extends Converter {
}
if (!to.startsWith(".")) to = `.${to}`;
this.activeConversions.delete(file.id);
worker.terminate();
return new VertFile(
new File([result.output], file.name),
result.isZip ? ".zip" : to,
);
} finally {
this.activeConversions.delete(file.id);
worker.terminate();
}
}
public async cancel(input: VertFile): Promise<void> {

View File

@ -723,11 +723,42 @@ export class VertdConverter extends Converter {
const apiUrl = await VertdInstance.instance.url();
return new Promise((resolve, reject) => {
let settled = false;
const protocol = apiUrl.startsWith("https") ? "wss:" : "ws:";
const ws = new WebSocket(
`${protocol}//${apiUrl.replace("http://", "").replace("https://", "")}/api/ws`,
);
const connectTimeout = setTimeout(() => {
if (settled) return;
settled = true;
this.activeConversions.delete(input.id);
ws.close();
reject(new Error("vertd websocket connection timeout"));
}, 30000);
const rejectConversion = (reason: unknown) => {
if (settled) return;
settled = true;
clearTimeout(connectTimeout);
this.activeConversions.delete(input.id);
if (
ws.readyState === WebSocket.CONNECTING ||
ws.readyState === WebSocket.OPEN
) {
ws.close();
}
reject(reason);
};
const resolveConversion = (value: VertFile) => {
if (settled) return;
settled = true;
clearTimeout(connectTimeout);
this.activeConversions.delete(input.id);
resolve(value);
};
this.activeConversions.set(input.id, {
ws,
jobId: uploadRes.id,
@ -735,6 +766,7 @@ export class VertdConverter extends Converter {
});
ws.onopen = () => {
clearTimeout(connectTimeout);
this.log(
`opened ws connection to vertd for file ${input.name}`,
);
@ -752,8 +784,32 @@ export class VertdConverter extends Converter {
this.log(`sent startJob message for file ${input.name}`);
};
ws.onerror = () => {
this.error(`ws error for file ${input.name}`);
rejectConversion(new Error("vertd websocket error"));
};
ws.onclose = (event) => {
if (settled) return;
this.error(
`ws closed unexpectedly for file ${input.name} (code: ${event.code})`,
);
rejectConversion(new Error("vertd websocket closed unexpectedly"));
};
ws.onmessage = async (e) => {
const msg: VertdMessage = JSON.parse(e.data);
let msg: VertdMessage;
try {
if (typeof e.data !== "string") {
rejectConversion(new Error("invalid websocket payload type"));
return;
}
msg = JSON.parse(e.data);
} catch {
rejectConversion(new Error("invalid websocket payload"));
return;
}
this.log(`received message ${msg.type} for file ${input.name}`);
switch (msg.type) {
case "progressUpdate": {
@ -781,10 +837,9 @@ export class VertdConverter extends Converter {
case "jobFinished": {
this.log(`job finished for file ${input.name}`);
ws.close();
this.activeConversions.delete(input.id);
try {
const url = `${apiUrl}/api/download/${msg.data.jobId}/${uploadRes.auth}`;
this.log(`downloading from ${url}`);
// const res = await fetch(url).then((res) => res.blob());
const res = await downloadFile(url, input);
// confirm download to clean up on server
@ -802,24 +857,28 @@ export class VertdConverter extends Converter {
this.error(`failed to confirm download: ${e}`);
}
resolve(new VertFile(new File([res], input.name), to));
resolveConversion(
new VertFile(new File([res], input.name), to),
);
} catch (e) {
if (hash) this.failure(hash);
rejectConversion(e);
}
break;
}
case "jobCancelled": {
this.log("job cancelled");
ws.close();
this.activeConversions.delete(input.id);
reject("Conversion cancelled");
rejectConversion("Conversion cancelled");
break;
}
case "error": {
this.error(`error: ${msg.data.message}`);
this.activeConversions.delete(input.id);
if (hash) this.failure(hash);
reject({
rejectConversion({
component: VertdErrorComponent,
additional: {
jobId: uploadRes.id,
@ -829,6 +888,11 @@ export class VertdConverter extends Converter {
errorMessage: msg.data.message,
},
});
break;
}
default: {
break;
}
}
};

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 { readSettings } from "$lib/util/settings";
import { VertdInstance } from "./vertdSettings.svelte";
export { default as Appearance } from "./Appearance.svelte";
@ -62,9 +63,9 @@ export class Settings {
public load() {
try {
VertdInstance.instance.load();
const ls = localStorage.getItem("settings");
if (!ls) return;
const settings: ISettings = JSON.parse(ls);
const persisted = readSettings<ISettings>();
if (!Object.keys(persisted).length) return;
const settings = persisted as ISettings;
const vertdBlockedHashes = new Map<string, Date[]>(
Object.entries(
settings.vertdBlockedHashes ||

View File

@ -11,6 +11,7 @@ import { m } from "$lib/paraglide/messages";
import sanitizeHtml from "sanitize-html";
import { ToastManager } from "$lib/util/toast.svelte";
import { GB } from "$lib/util/consts";
import { readSettings } from "$lib/util/settings";
class Files {
public files = $state<VertFile[]>([]);
@ -52,9 +53,7 @@ class Files {
public requiredConverters = $derived(
Array.from(
new Set(
this.files.flatMap((file) =>
this.getRequiredConverters(file),
),
this.files.flatMap((file) => this.getRequiredConverters(file)),
),
),
);
@ -93,6 +92,9 @@ class Files {
?.includes(file.from.toLowerCase());
try {
if (file.blobUrl?.startsWith("blob:"))
URL.revokeObjectURL(file.blobUrl);
if (isAudio) {
// try to get the thumbnail from the audio via music-metadata
const { common } = await parseBlob(file.file, {
@ -136,8 +138,10 @@ class Files {
const mediaElement = isVideo
? document.createElement("video")
: new Image();
mediaElement.src = URL.createObjectURL(file);
const mediaUrl = URL.createObjectURL(file);
mediaElement.src = mediaUrl;
try {
await new Promise((resolve, reject) => {
if (isVideo) {
const video = mediaElement as HTMLVideoElement;
@ -171,7 +175,12 @@ class Files {
ctx.drawImage(mediaElement, 0, 0, canvas.width, canvas.height);
// check if completely transparent
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const imageData = ctx.getImageData(
0,
0,
canvas.width,
canvas.height,
);
const isTransparent = Array.from(imageData.data).every(
(value, index) => {
return (index + 1) % 4 !== 0 || value === 0;
@ -185,6 +194,9 @@ class Files {
const url = canvas.toDataURL();
canvas.remove();
return url;
} finally {
URL.revokeObjectURL(mediaUrl);
}
}
private async _handleZipFile(file: File): Promise<void> {
@ -435,7 +447,7 @@ class Files {
const blob = await downloadZip(dlFiles, "converted.zip").blob();
const url = URL.createObjectURL(blob);
const settings = JSON.parse(localStorage.getItem("settings") ?? "{}");
const settings = readSettings<{ filenameFormat?: string }>();
const filenameFormat = settings.filenameFormat || "VERT_%name%";
const format = (name: string) => {
@ -603,7 +615,10 @@ export const getMaxArrayBufferSize = (): number => {
// lmao uh mobile devices definitely have a much lower limit and using binary search here
// was causing crashes especially on iOS, so just return 2GB to be safe :p
if (get(isMobile)) {
log(["converters"], `mobile device likely detected, using 2GB fallback for max ArrayBuffer size`);
log(
["converters"],
`mobile device likely detected, using 2GB fallback for max ArrayBuffer size`,
);
// don't save to localStorage, since it can always be a false positive or the user's browser window is simply just small
return 2 * GB;
}

View File

@ -11,6 +11,7 @@ import type {
SettingDefinition,
} from "./conversion-settings";
import { log } from "$lib/util/logger";
import { readSettings } from "$lib/util/settings";
const MAX_BLOB_SIZE_LIMIT = 2 * 1024 * 1024 * 1024; // 2GB
@ -121,7 +122,7 @@ export class VertFile {
constructor(file: File, to: string, blobUrl?: string) {
const ext = file.name.split(".").pop();
const newFile = new File(
[file.slice(0, file.size, file.type)],
[file],
`${file.name.split(".").slice(0, -1).join(".")}.${ext?.toLowerCase()}`,
);
this.file = newFile;
@ -479,7 +480,7 @@ export class VertFile {
let to = this.result.to;
if (!to.startsWith(".")) to = `.${to}`;
const settings = JSON.parse(localStorage.getItem("settings") ?? "{}");
const settings = readSettings<{ filenameFormat?: string }>();
const filenameFormat = settings.filenameFormat || "VERT_%name%";
const format = (name: string) => {
@ -528,7 +529,7 @@ export class VertFile {
setTimeout(() => {
cache.delete(cacheKey);
}, 3000);
}, 30000);
} else {
blob = URL.createObjectURL(
new Blob([await this.result.file.arrayBuffer()], {
@ -545,7 +546,9 @@ export class VertFile {
a.target = "_blank";
a.style.display = "none";
a.click();
setTimeout(() => {
URL.revokeObjectURL(blob);
}, 30000);
a.remove();
}

23
src/lib/util/settings.ts Normal file
View File

@ -0,0 +1,23 @@
import { browser } from "$app/environment";
import { error } from "$lib/util/logger";
export function readSettings<T extends object = Record<string, unknown>>(): Partial<T> {
if (!browser) return {};
const raw = localStorage.getItem("settings");
if (!raw) return {};
try {
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
localStorage.removeItem("settings");
return {};
}
return parsed as Partial<T>;
} catch (e) {
error(["settings", "error"], `failed to parse saved settings: ${e}`);
localStorage.removeItem("settings");
return {};
}
}

View File

@ -46,15 +46,20 @@ class ServiceWorkerManager {
return new Promise((resolve, reject) => {
const messageChannel = new MessageChannel();
messageChannel.port1.onmessage = (event) => {
resolve(event.data);
};
setTimeout(() => {
let settled = false;
const timeoutId = setTimeout(() => {
if (settled) return;
settled = true;
reject(new Error("Timeout waiting for cache info"));
}, 5000);
messageChannel.port1.onmessage = (event) => {
if (settled) return;
settled = true;
clearTimeout(timeoutId);
resolve(event.data);
};
navigator.serviceWorker?.controller?.postMessage(
{ type: "GET_CACHE_INFO" },
[messageChannel.port2],
@ -69,8 +74,17 @@ class ServiceWorkerManager {
return new Promise((resolve, reject) => {
const messageChannel = new MessageChannel();
let settled = false;
const timeoutId = setTimeout(() => {
if (settled) return;
settled = true;
reject(new Error("Timeout waiting for cache clear"));
}, 10000);
messageChannel.port1.onmessage = (event) => {
if (settled) return;
settled = true;
clearTimeout(timeoutId);
if (event.data.success) {
resolve();
} else {
@ -80,10 +94,6 @@ class ServiceWorkerManager {
}
};
setTimeout(() => {
reject(new Error("Timeout waiting for cache clear"));
}, 10000);
navigator.serviceWorker?.controller?.postMessage(
{ type: "CLEAR_CACHE" },
[messageChannel.port2],

View File

@ -76,9 +76,16 @@
// Check if the data is already in sessionStorage
const cachedContribs = sessionStorage.getItem("ghContribs");
if (cachedContribs) {
ghContribs = JSON.parse(cachedContribs);
try {
const parsedContribs = JSON.parse(cachedContribs);
if (Array.isArray(parsedContribs)) {
ghContribs = parsedContribs;
return;
}
} catch {
sessionStorage.removeItem("ghContribs");
}
}
// Fetch GitHub contributors
try {
@ -104,30 +111,16 @@
!excludedNames.has(contrib.login),
);
// Fetch and cache avatar images as Base64
const fetchAvatar = async (url: string) => {
const res = await fetch(url);
const blob = await res.blob();
return new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result as string);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
};
ghContribs = await Promise.all(
filteredContribs.map(
async (contrib: {
ghContribs = filteredContribs.map(
(contrib: {
login: string;
avatar_url: string;
html_url: string;
}) => ({
name: contrib.login,
avatar: await fetchAvatar(contrib.avatar_url),
avatar: contrib.avatar_url,
github: contrib.html_url,
}),
),
);
// Cache the data in sessionStorage

View File

@ -2,17 +2,22 @@
import { browser } from "$app/environment";
import { error, log } from "$lib/util/logger";
import * as Settings from "$lib/sections/settings/index.svelte";
import { PUB_PLAUSIBLE_URL } from "$env/static/public";
import { SettingsIcon } from "lucide-svelte";
import { onMount } from "svelte";
import { m } from "$lib/paraglide/messages";
import { ToastManager } from "$lib/util/toast.svelte";
import { DISABLE_ALL_EXTERNAL_REQUESTS } from "$lib/util/consts";
import { readSettings } from "$lib/util/settings";
let settings = $state(Settings.Settings.instance.settings);
let isInitial = $state(true);
const readSavedSettings = () => {
const parsed = readSettings<typeof settings>();
return Object.keys(parsed).length ? parsed : null;
};
$effect(() => {
if (!browser) return;
if (isInitial) {
@ -20,12 +25,9 @@
return;
}
const savedSettings = localStorage.getItem("settings");
if (savedSettings) {
const parsedSettings = JSON.parse(savedSettings);
if (JSON.stringify(parsedSettings) === JSON.stringify(settings))
const parsedSettings = readSavedSettings();
if (parsedSettings && JSON.stringify(parsedSettings) === JSON.stringify(settings))
return;
}
try {
Settings.Settings.instance.settings = settings;
@ -41,9 +43,8 @@
});
onMount(() => {
const savedSettings = localStorage.getItem("settings");
if (savedSettings) {
const parsedSettings = JSON.parse(savedSettings);
const parsedSettings = readSavedSettings();
if (parsedSettings) {
Settings.Settings.instance.settings = {
...Settings.Settings.instance.settings,
...parsedSettings,

View File

@ -126,8 +126,10 @@ self.addEventListener("fetch", (event) => {
self.addEventListener("message", (event) => {
if (!event.data) return;
const type = event.data.type;
const port = event.ports?.[0];
if (type === "GET_CACHE_INFO") {
if (!port) return;
event.waitUntil(
caches.open(CACHE_NAME).then(async (cache) => {
const keys = await cache.keys();
@ -159,7 +161,7 @@ self.addEventListener("message", (event) => {
}
}
event.ports[0].postMessage({
port.postMessage({
totalSize,
fileCount: files.length,
files,
@ -169,6 +171,7 @@ self.addEventListener("message", (event) => {
}
if (type === "CLEAR_CACHE") {
if (!port) return;
event.waitUntil(
caches
.delete(CACHE_NAME)
@ -177,11 +180,11 @@ self.addEventListener("message", (event) => {
return caches.open(CACHE_NAME);
})
.then(() => {
event.ports[0].postMessage({ success: true });
port.postMessage({ success: true });
})
.catch((err) => {
console.error("[SW] failed to clear cache:", err);
event.ports[0].postMessage({
port.postMessage({
success: false,
error: err.message,
});