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 {