feat: use & rework findConverters()

adds a priority to FormatInfo which ensures that we can have certain formats prioritize certain converters (mp4 supports mediabunny/local so use that, then fallback to next if fails)
This commit is contained in:
Maya 2026-03-06 12:00:43 +03:00
parent 343fb34a0e
commit 96da202e6c
No known key found for this signature in database
12 changed files with 260 additions and 102 deletions

View File

@ -12,17 +12,18 @@
"@fontsource/lexend": "^5.2.11",
"@fontsource/radio-canada-big": "^5.2.7",
"@imagemagick/magick-wasm": "^0.0.37",
"@stripe/stripe-js": "^8.7.0",
"@mediabunny/ac3": "^1.35.1",
"@mediabunny/flac-encoder": "^1.37.0",
"@mediabunny/mp3-encoder": "^1.35.1",
"@stripe/stripe-js": "^8.7.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",
"overlayscrollbars": "^2.14.0",
"mediabunny": "^1.35.1",
"overlayscrollbars-svelte": "^0.5.5",
"p-queue": "^9.1.0",
"riff-file": "^1.0.3",
@ -180,6 +181,8 @@
"@mediabunny/ac3": ["@mediabunny/ac3@1.35.1", "", { "peerDependencies": { "mediabunny": "^1.0.0" } }, "sha512-gLx3mFfs58/cdz2/f5Fp+6ZOrX5Jli3AZMXw/5EJcgm2VpnC/2oxtJyP1x/00PIS4UCE770slwIdz7U+2CQ31g=="],
"@mediabunny/flac-encoder": ["@mediabunny/flac-encoder@1.37.0", "", { "peerDependencies": { "mediabunny": "^1.0.0" } }, "sha512-VwKIL5p1WZE4dSwZ1SVv/bd2ksul8a4run4S1eEbPRysnG87nmCXddO5ajD3b2k2478XWitKnVDXl/kxdIIWBw=="],
"@mediabunny/mp3-encoder": ["@mediabunny/mp3-encoder@1.35.1", "", { "peerDependencies": { "mediabunny": "^1.0.0" } }, "sha512-iY6FcPs7GbHMs/ASPmdzwojKcBN4AfMa+zFh4KNZNaLToyR7aEZILj9FsPVJA11bshaoo80dTaBcn69i33JHVA=="],
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
@ -640,7 +643,7 @@
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
"mediabunny": ["mediabunny@1.35.1", "", { "dependencies": { "@types/dom-mediacapture-transform": "^0.1.11", "@types/dom-webcodecs": "0.1.13" } }, "sha512-VrprpjkLTZyIyhzBAc9D3HqgXarAE+le7+6x0Sdu9WN2SD86L8bUy0hz06Xwf14dVPqS7OwpY2KOhlUyqmI2eQ=="],
"mediabunny": ["mediabunny@1.37.0", "", { "dependencies": { "@types/dom-mediacapture-transform": "^0.1.11", "@types/dom-webcodecs": "0.1.13" } }, "sha512-eV7M9IJ29pr/8RNL1sYtIxNbdMfDMN1hMwMaOFfNLhwuKKGSC+eKwiJFpdVjEJ3zrMA4LGerF4Hps0SENFSAlg=="],
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],

View File

@ -1,33 +1,39 @@
import prettier from 'eslint-config-prettier';
import js from '@eslint/js';
import svelte from 'eslint-plugin-svelte';
import globals from 'globals';
import ts from 'typescript-eslint';
import prettier from "eslint-config-prettier";
import js from "@eslint/js";
import svelte from "eslint-plugin-svelte";
import globals from "globals";
import ts from "typescript-eslint";
export default ts.config(
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs['flat/recommended'],
...svelte.configs["flat/recommended"],
prettier,
...svelte.configs['flat/prettier'],
...svelte.configs["flat/prettier"],
{
languageOptions: {
globals: {
...globals.browser,
...globals.node
}
}
...globals.node,
},
},
},
{
files: ['**/*.svelte'],
files: ["**/*.svelte"],
languageOptions: {
parserOptions: {
parser: ts.parser
}
}
parser: ts.parser,
},
},
},
{
ignores: ['build/', '.svelte-kit/', 'dist/']
}
ignores: ["build/", ".svelte-kit/", "dist/"],
},
{
files: ["**/*.ts", "**/*.svelte.ts"],
rules: {
"no-at-html-tags": "off",
},
},
);

View File

@ -45,18 +45,19 @@
"@fontsource/lexend": "^5.2.11",
"@fontsource/radio-canada-big": "^5.2.7",
"@imagemagick/magick-wasm": "^0.0.37",
"@stripe/stripe-js": "^8.7.0",
"@mediabunny/ac3": "^1.35.1",
"@mediabunny/flac-encoder": "^1.37.0",
"@mediabunny/mp3-encoder": "^1.35.1",
"@stripe/stripe-js": "^8.7.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",
"overlayscrollbars": "^2.14.0",
"overlayscrollbars-svelte": "^0.5.5",
"mediabunny": "^1.35.1",
"p-queue": "^9.1.0",
"riff-file": "^1.0.3",
"sanitize-html": "^2.17.0",

View File

@ -72,9 +72,10 @@
// decide which converters to use to detect category:
// - if file provided, prefer its primary converter -- individual file dropdown
// - if no file provided, use all converters from all files -- "set all to" dropdown
const primaryConverter = file ? (file.isZip() ? file.converters[0] : file.findConverters()[0]) : null;
const convertersToCheck = file
? file.findConverter()
? [file.findConverter()!]
? primaryConverter
? [primaryConverter]
: file.converters
: files.files.flatMap((f) => f.converters);

View File

@ -1,4 +1,5 @@
<script lang="ts">
/* eslint-disable @typescript-eslint/no-explicit-any */
import { SearchIcon } from "lucide-svelte";
import Dropdown from "./Dropdown.svelte";
import FancyInput from "./FancyInput.svelte";
@ -26,7 +27,9 @@
const applySettings = async () => {
onclose?.();
if (!file) return;
const converter = file.findConverter();
const converter = file.isZip()
? file.converters[0]
: file.findConverters()[0];
if (!converter) {
log(
["settings", "modal"],
@ -72,8 +75,10 @@
<p class="text-base">
{@html sanitize(
m["convert.settings.description"]({
converter:
file.findConverter()?.name || "unknown",
converter: file.isZip()
? file.converters[0].name
: file.findConverters()[0].name ||
"unknown",
filename: file.name,
}),
)}

View File

@ -16,6 +16,7 @@ export class FormatInfo {
public fromSupported = true,
public toSupported = true,
public isNative = true,
public priority = 1,
) {
this.name = name;
if (!this.name.startsWith(".")) {

View File

@ -74,7 +74,7 @@ export class FFmpegConverter extends Converter {
new FormatInfo("m4b", true, true),
new FormatInfo("voc", true, true),
new FormatInfo("weba", true, true),
...videoFormats.map((f) => new FormatInfo(f, true, true, false)),
...videoFormats.map((f) => new FormatInfo(f, true, true, false, 0)),
];
public readonly reportsProgress = true;

View File

@ -11,15 +11,12 @@ const getConverters = (): Converter[] => {
const converters: Converter[] = [
new MagickConverter(),
new FFmpegConverter(),
new PandocConverter(),
new MediabunnyConverter(),
];
if (DISABLE_ALL_EXTERNAL_REQUESTS) {
converters.push(new VertdConverter());
}
if (!DISABLE_ALL_EXTERNAL_REQUESTS) converters.push(new VertdConverter());
converters.push(new MediabunnyConverter());
converters.push(new PandocConverter());
return converters;
};

View File

@ -22,31 +22,147 @@ import { registerAc3Decoder, registerAc3Encoder } from "@mediabunny/ac3";
import { Converter, FormatInfo, type WorkerStatus } from "./converter.svelte";
import { ToastManager } from "$lib/util/toast.svelte";
import { error, log } from "$lib/util/logger";
import { registerFlacEncoder } from "@mediabunny/flac-encoder";
// codec compatibility object, based on docs
// https://mediabunny.dev/guide/supported-formats-and-codecs#compatibility-table
const codecCompatibility = {
video: {
mp4: ['avc', 'hevc', 'vp8', 'vp9', 'av1'],
m4v: ['avc', 'hevc', 'vp8', 'vp9', 'av1'],
f4v: ['avc', 'hevc', 'vp8', 'vp9', 'av1'],
'3gp': ['avc', 'hevc', 'vp8', 'vp9', 'av1'],
'3g2': ['avc', 'hevc', 'vp8', 'vp9', 'av1'],
mkv: ['avc', 'hevc', 'vp8', 'vp9', 'av1'],
webm: ['vp8', 'vp9', 'av1'],
mov: ['avc', 'hevc', 'vp8', 'vp9', 'av1'],
ts: ['avc', 'hevc'],
mp4: ["avc", "hevc", "vp8", "vp9", "av1"],
m4v: ["avc", "hevc", "vp8", "vp9", "av1"],
f4v: ["avc", "hevc", "vp8", "vp9", "av1"],
"3gp": ["avc", "hevc", "vp8", "vp9", "av1"],
"3g2": ["avc", "hevc", "vp8", "vp9", "av1"],
mkv: ["avc", "hevc", "vp8", "vp9", "av1"],
webm: ["vp8", "vp9", "av1"],
mov: ["avc", "hevc", "vp8", "vp9", "av1"],
ts: ["avc", "hevc"],
},
audio: {
mp4: ['aac', 'opus', 'mp3', 'vorbis', 'flac', 'ac3', 'eac3', 'pcm-s16', 'pcm-s16be', 'pcm-s24', 'pcm-s24be', 'pcm-s32', 'pcm-s32be', 'pcm-f32', 'pcm-f64'],
m4v: ['aac', 'opus', 'mp3', 'vorbis', 'flac', 'ac3', 'eac3', 'pcm-s16', 'pcm-s16be', 'pcm-s24', 'pcm-s24be', 'pcm-s32', 'pcm-s32be', 'pcm-f32', 'pcm-f64'],
f4v: ['aac', 'opus', 'mp3', 'vorbis', 'flac', 'ac3', 'eac3', 'pcm-s16', 'pcm-s16be', 'pcm-s24', 'pcm-s24be', 'pcm-s32', 'pcm-s32be', 'pcm-f32', 'pcm-f64'],
'3gp': ['aac', 'opus', 'mp3', 'vorbis', 'flac', 'ac3', 'eac3', 'pcm-s16', 'pcm-s16be', 'pcm-s24', 'pcm-s24be', 'pcm-s32', 'pcm-s32be', 'pcm-f32', 'pcm-f64'],
'3g2': ['aac', 'opus', 'mp3', 'vorbis', 'flac', 'ac3', 'eac3', 'pcm-s16', 'pcm-s16be', 'pcm-s24', 'pcm-s24be', 'pcm-s32', 'pcm-s32be', 'pcm-f32', 'pcm-f64'],
mkv: ['aac', 'opus', 'mp3', 'vorbis', 'flac', 'ac3', 'eac3', 'pcm-u8', 'pcm-s16', 'pcm-s24', 'pcm-s32', 'pcm-f32', 'pcm-f64'],
webm: ['opus', 'vorbis'],
mov: ['aac', 'opus', 'mp3', 'vorbis', 'flac', 'ac3', 'eac3', 'pcm-u8', 'pcm-s8', 'pcm-s16', 'pcm-s16be', 'pcm-s24', 'pcm-s24be', 'pcm-s32', 'pcm-s32be', 'pcm-f32', 'pcm-f32be', 'pcm-f64', 'ulaw', 'alaw'],
ts: ['aac', 'mp3', 'ac3', 'eac3'],
mp4: [
"aac",
"opus",
"mp3",
"vorbis",
"flac",
"ac3",
"eac3",
"pcm-s16",
"pcm-s16be",
"pcm-s24",
"pcm-s24be",
"pcm-s32",
"pcm-s32be",
"pcm-f32",
"pcm-f64",
],
m4v: [
"aac",
"opus",
"mp3",
"vorbis",
"flac",
"ac3",
"eac3",
"pcm-s16",
"pcm-s16be",
"pcm-s24",
"pcm-s24be",
"pcm-s32",
"pcm-s32be",
"pcm-f32",
"pcm-f64",
],
f4v: [
"aac",
"opus",
"mp3",
"vorbis",
"flac",
"ac3",
"eac3",
"pcm-s16",
"pcm-s16be",
"pcm-s24",
"pcm-s24be",
"pcm-s32",
"pcm-s32be",
"pcm-f32",
"pcm-f64",
],
"3gp": [
"aac",
"opus",
"mp3",
"vorbis",
"flac",
"ac3",
"eac3",
"pcm-s16",
"pcm-s16be",
"pcm-s24",
"pcm-s24be",
"pcm-s32",
"pcm-s32be",
"pcm-f32",
"pcm-f64",
],
"3g2": [
"aac",
"opus",
"mp3",
"vorbis",
"flac",
"ac3",
"eac3",
"pcm-s16",
"pcm-s16be",
"pcm-s24",
"pcm-s24be",
"pcm-s32",
"pcm-s32be",
"pcm-f32",
"pcm-f64",
],
mkv: [
"aac",
"opus",
"mp3",
"vorbis",
"flac",
"ac3",
"eac3",
"pcm-u8",
"pcm-s16",
"pcm-s24",
"pcm-s32",
"pcm-f32",
"pcm-f64",
],
webm: ["opus", "vorbis"],
mov: [
"aac",
"opus",
"mp3",
"vorbis",
"flac",
"ac3",
"eac3",
"pcm-u8",
"pcm-s8",
"pcm-s16",
"pcm-s16be",
"pcm-s24",
"pcm-s24be",
"pcm-s32",
"pcm-s32be",
"pcm-f32",
"pcm-f32be",
"pcm-f64",
"ulaw",
"alaw",
],
ts: ["aac", "mp3", "ac3", "eac3"],
},
} as const;
@ -57,18 +173,21 @@ export class MediabunnyConverter extends Converter {
private activeConversions = new Map<string, Conversion>();
public supportedFormats: FormatInfo[] = [
new FormatInfo("mp4", true, true),
new FormatInfo("m4v", true, true),
new FormatInfo("mkv", true, true),
new FormatInfo("webm", true, true),
new FormatInfo("mov", true, true),
private formats: string[] = [
"mp4",
"m4v",
"mkv",
"webm",
"mov",
"f4v",
"3gp",
"3g2",
"mts",
"ts",
];
// mp4-based formats (should work)
new FormatInfo("f4v", true, true),
new FormatInfo("3gp", true, true),
new FormatInfo("3g2", true, true),
new FormatInfo("ts", true, true),
public supportedFormats: FormatInfo[] = [
...this.formats.map((f) => new FormatInfo(f, true, true, true, 2)),
];
constructor() {
@ -81,9 +200,11 @@ export class MediabunnyConverter extends Converter {
private async initializeCodecs(): Promise<void> {
if (!(await canEncodeAudio("mp3"))) {
// Only register the custom encoder if there's no native support
registerMp3Encoder();
}
if (!(await canEncodeAudio("flac"))) {
registerFlacEncoder();
}
registerAc3Decoder();
registerAc3Encoder();
}

View File

@ -298,8 +298,8 @@ export class VertdConverter extends Converter {
>();
public supportedFormats = [
new FormatInfo("mkv", true, true),
new FormatInfo("mp4", true, true),
new FormatInfo("mkv", true, true),
new FormatInfo("webm", true, true),
new FormatInfo("avi", true, true),
new FormatInfo("wmv", true, true),

View File

@ -23,7 +23,7 @@ export class VertFile {
return this.file.name;
}
public conversionSettings = $state<ConversionSettings>({}); // empty object = defaults
public conversionSettings = $state<ConversionSettings>({}); // empty object / key = default
public progress = $state(0);
public result = $state<VertFile | null>(null);
@ -40,45 +40,55 @@ export class VertFile {
public isZip = $state(() => this.from === ".zip");
public getAvailableSettings(input: VertFile): Promise<SettingDefinition[]> {
const converter = this.findConverter();
const converter = this.findConverters()[0];
if (!converter) return Promise.resolve([]);
return converter.getAvailableSettings(input);
}
public findConverters(supportedFormats: string[] = [this.from]) {
const converter = this.converters
.filter((converter) =>
converter
.formatStrings()
.map((f) => supportedFormats.includes(f)),
)
.sort(byNative(this.from));
return converter;
}
return this.converters
.filter((converter) => {
if (
!converter
.formatStrings()
.some((f) => supportedFormats.includes(f))
) {
return false;
}
public findConverter() {
// zip will always only be added if there's one converter that supports all files - handled in store's _handleZipFile()
if (this.isZip()) return this.converters[0];
if (
supportedFormats.includes(this.from) &&
supportedFormats.includes(this.to)
) {
if (!converter.formatStrings().includes(this.to)) {
return false;
}
const converter = this.converters.find((converter) => {
if (
!converter.formatStrings().includes(this.from) ||
!converter.formatStrings().includes(this.to)
) {
return false;
}
const theirFrom = converter.supportedFormats.find(
(f) => f.name === this.from,
);
const theirTo = converter.supportedFormats.find(
(f) => f.name === this.to,
);
if (!theirFrom || !theirTo) return false;
if (!theirFrom.isNative && !theirTo.isNative) return false;
}
const theirFrom = converter.supportedFormats.find(
(f) => f.name === this.from,
);
const theirTo = converter.supportedFormats.find(
(f) => f.name === this.to,
);
if (!theirFrom || !theirTo) return false;
if (!theirFrom.isNative && !theirTo.isNative) return false;
return true;
});
return converter;
return true;
})
.sort(byNative(this.from))
.sort((a, b) => {
// sort by priority of format
const aFrom = a.supportedFormats.find(
(f) => f.name === this.from,
);
const bFrom = b.supportedFormats.find(
(f) => f.name === this.from,
);
const aPriority = aFrom ? aFrom.priority : 1;
const bPriority = bFrom ? bFrom.priority : 1;
return bPriority - aPriority;
});
}
public isLarge(): boolean {
@ -88,7 +98,9 @@ export class VertFile {
public supportsStreaming(): boolean {
// only vertd (video/gif -> video/gif) supports streaming
// rest of converters need entire file in memory, limited by ArrayBuffer limits
const converter = this.findConverter();
const converter = this.isZip()
? this.converters[0]
: this.findConverters()[0];
return converter?.name === "vertd";
}
@ -106,12 +118,21 @@ export class VertFile {
this.convert = this.convert.bind(this);
this.download = this.download.bind(this);
this.blobUrl = blobUrl;
console.log(`VertFile: ${this.name}`);
console.log(
`findConverters: ${this.findConverters()
.map((c) => c.name)
.join(", ")}`,
);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public async convert(...args: any[]) {
if (!this.converters.length) throw new Error("No converters found");
const converter = this.findConverter();
const converter = this.isZip()
? this.converters[0]
: this.findConverters()[0];
if (!converter) throw new Error("No converter found");
this.result = null;
this.progress = 0;
@ -231,7 +252,9 @@ export class VertFile {
public async cancel() {
if (!this.processing) return;
const converter = this.findConverter();
const converter = this.isZip()
? this.converters[0]
: this.findConverters()[0];
if (!converter) throw new Error("No converter found");
this.cancelled = true;
try {

View File

@ -42,7 +42,7 @@
const settings = Settings.instance.settings;
if (processedFileIds.has(file.id)) return;
const converter = file.findConverter();
const converter = file.isZip() ? file.converters[0] : file.findConverters()[0];
if (!converter) return;
let category: string | undefined;
@ -108,7 +108,7 @@
let type = "";
if (files.files.length) {
const converters = files.files.map(
(file) => file.findConverter()?.name,
(file) => (file.isZip() ? file.converters[0] : file.findConverters()[0])?.name,
);
const uniqueTypes = new Set(converters);
@ -130,7 +130,7 @@
</script>
{#snippet fileItem(file: VertFile, index: number)}
{@const currentConverter = file.findConverter()}
{@const currentConverter = file.isZip() ? file.converters[0] : file.findConverters()[0]}
{@const isImage = currentConverter?.name === "imagemagick"}
{@const isAudio = currentConverter?.name === "ffmpeg"}
{@const isVideo = currentConverter?.name === "vertd"}