From 9dfbfe64128f6f17b111933afe6817b653c47875 Mon Sep 17 00:00:00 2001 From: not-nullptr Date: Thu, 13 Nov 2025 17:57:52 +0000 Subject: [PATCH] feat: block repeat failures (i really hope this functions because i wrote it a month ago) --- messages/en.json | 1 + messages/tr.json | 5 +- src/lib/converters/vertd.svelte.ts | 57 ++++++++++++++++++----- src/lib/sections/settings/index.svelte.ts | 10 ++++ src/lib/types/file.svelte.ts | 35 +++++++++++++- 5 files changed, 92 insertions(+), 16 deletions(-) diff --git a/messages/en.json b/messages/en.json index 4cee4ac..bcb0749 100644 --- a/messages/en.json +++ b/messages/en.json @@ -98,6 +98,7 @@ "vertd_details_to": "To format: {to}", "vertd_details_error_message": "Error message: [view_link]View error logs[/view_link]", "vertd_details_close": "Close", + "vertd_ratelimit": "Your video, '{filename}', has failed to convert a few times. To prevent server overload, further conversion attempts for this file have been temporarily blocked. Please try again later.", "unsupported_format": "Only image, video, audio, and document files are supported", "format_output_only": "This format can currently only be used as output (converted to), not as input.", "vertd_not_found": "Could not find the vertd instance to start video conversion. Are you sure the instance URL is set correctly?", diff --git a/messages/tr.json b/messages/tr.json index 4b1fa07..7aacdc0 100644 --- a/messages/tr.json +++ b/messages/tr.json @@ -86,7 +86,6 @@ "audio": "ses", "doc": "belge", "image": "görsel" - } }, "settings": { @@ -228,8 +227,8 @@ }, "workers": { "errors": { - "general": "{dosya} dönüştürülürken hata oluştu: {message}", - "cancel": "{dosya} için dönüştürme işlemi iptal edilirken hata oluştu: {message}", + "general": "{file} dönüştürülürken hata oluştu: {message}", + "cancel": "{file} için dönüştürme işlemi iptal edilirken hata oluştu: {message}", "magick": "Magick işlemi sırasında hata oluştu, görsel dönüştürme işlemi beklendiği gibi çalışmayabilir.", "ffmpeg": "ffmpeg yüklenirken hata oluştu, bazı özellikler çalışmayabilir.", "no_audio": "Ses akışı bulunamadı.", diff --git a/src/lib/converters/vertd.svelte.ts b/src/lib/converters/vertd.svelte.ts index 2db5c72..c93861f 100644 --- a/src/lib/converters/vertd.svelte.ts +++ b/src/lib/converters/vertd.svelte.ts @@ -1,22 +1,11 @@ import VertdErrorComponent from "$lib/components/functional/VertdError.svelte"; import { error, log } from "$lib/logger"; +import { m } from "$lib/paraglide/messages"; 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"; -interface VertdError { - type: "error"; - data: string; -} - -interface VertdSuccess { - type: "success"; - data: T; -} - -type VertdResponse = VertdError | VertdSuccess; - interface UploadResponse { id: string; auth: string; @@ -49,6 +38,7 @@ export const vertdFetch: { url: U, options: RequestInit, ): Promise; + // eslint-disable-next-line @typescript-eslint/no-explicit-any } = async (url: any, options: RequestInit, body?: any) => { const domain = await VertdInstance.instance.url(); @@ -298,9 +288,50 @@ export class VertdConverter extends Converter { this.status = "ready"; } + private blocked(hash: string): boolean { + const blockedHashes = Settings.instance.settings.vertdBlockedHashes; + + const now = new Date(); + const dates = blockedHashes.get(hash) || []; + const filteredDates = dates.filter( + (date) => now.getTime() - date.getTime() < 60 * 60 * 1000, + ); + + if (filteredDates.length === 0) { + blockedHashes.delete(hash); + return false; + } + + blockedHashes.set(hash, filteredDates); + + Settings.instance.save(); + + return filteredDates.length >= 3; + } + + private failure(hash: string): void { + const blockedHashes = Settings.instance.settings.vertdBlockedHashes; + const now = new Date(); + const dates = blockedHashes.get(hash) || []; + dates.push(now); + blockedHashes.set(hash, dates); + Settings.instance.save(); + } + public async convert(input: VertFile, to: string): Promise { if (to.startsWith(".")) to = to.slice(1); + const hash = await input.hash(); + + if (this.blocked(hash)) { + this.log(`conversion blocked for file ${input.name}`); + throw new Error( + m["convert.errors.vertd_ratelimit"]({ + filename: input.name, + }), + ); + } + const uploadRes = await uploadFile(input); const apiUrl = await VertdInstance.instance.url(); @@ -372,6 +403,8 @@ export class VertdConverter extends Converter { case "error": { this.log(`error: ${msg.data.message}`); this.activeConversions.delete(input.id); + this.failure(hash); + reject({ component: VertdErrorComponent, additional: { diff --git a/src/lib/sections/settings/index.svelte.ts b/src/lib/sections/settings/index.svelte.ts index a3eacc8..91e41b1 100644 --- a/src/lib/sections/settings/index.svelte.ts +++ b/src/lib/sections/settings/index.svelte.ts @@ -28,6 +28,7 @@ export interface ISettings { ffmpegQuality: ConversionBitrate; // audio (or audio <-> video) ffmpegSampleRate: string; // audio (or audio <-> video) ffmpegCustomSampleRate: number; // audio (or audio <-> video) - only used when ffmpegSampleRate is "custom" + vertdBlockedHashes: Map; // hashes of files blocked from vertd conversion } export class Settings { @@ -50,6 +51,7 @@ export class Settings { ffmpegQuality: "auto", ffmpegSampleRate: "auto", ffmpegCustomSampleRate: 44100, + vertdBlockedHashes: new Map(), }); public save() { @@ -62,6 +64,14 @@ export class Settings { const ls = localStorage.getItem("settings"); if (!ls) return; const settings: ISettings = JSON.parse(ls); + const vertdBlockedHashes = new Map( + Object.entries( + settings.vertdBlockedHashes || this.settings.vertdBlockedHashes, + ), + ); + + settings.vertdBlockedHashes = vertdBlockedHashes; + this.settings = { ...this.settings, ...settings, diff --git a/src/lib/types/file.svelte.ts b/src/lib/types/file.svelte.ts index 7501a43..8e556ae 100644 --- a/src/lib/types/file.svelte.ts +++ b/src/lib/types/file.svelte.ts @@ -1,6 +1,5 @@ import { byNative, converters } from "$lib/converters"; import type { Converter } from "$lib/converters/converter.svelte"; -import { error } from "$lib/logger"; import { m } from "$lib/paraglide/messages"; import { ToastManager } from "$lib/toast/index.svelte"; import type { Component } from "svelte"; @@ -196,6 +195,40 @@ export class VertFile { URL.revokeObjectURL(blob); a.remove(); } + + public hash(): Promise { + const stream = this.file.stream(); + const hashes = new Set(); + const reader = stream.getReader(); + return new Promise((resolve, reject) => { + function processChunk() { + reader.read().then(({ done, value }) => { + if (done) { + const combinedHash = Array.from(hashes).sort().join(""); + resolve(combinedHash); + return; + } + + crypto.subtle + .digest("SHA-256", value) + .then((hashBuffer) => { + const hashArray = Array.from( + new Uint8Array(hashBuffer), + ); + const hashHex = hashArray + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); + hashes.add(hashHex); + processChunk(); + }) + .catch((err) => { + reject(err); + }); + }); + } + processChunk(); + }); + } } export interface Categories {