feat: new* conversion resolver, vertd gif support

This commit is contained in:
not-nullptr 2025-03-19 16:35:29 +00:00
parent 0e62d79a23
commit c00dac9207
6 changed files with 90 additions and 48 deletions

View File

@ -81,7 +81,7 @@ export class FFmpegConverter extends Converter {
window.plausible("convert", { window.plausible("convert", {
props: { props: {
type: "audio", type: "audio",
} },
}); });
ffmpeg.terminate(); ffmpeg.terminate();
return new VertFile(new File([output], input.name), to); return new VertFile(new File([output], input.name), to);

View File

@ -193,7 +193,15 @@ export class VertdConverter extends Converter {
public name = "vertd"; public name = "vertd";
public ready = $state(false); public ready = $state(false);
public reportsProgress = true; public reportsProgress = true;
public supportedFormats = [".mkv", ".mp4", ".webm", ".avi", ".wmv", ".mov"]; public supportedFormats = [
".mkv",
".mp4",
".webm",
".avi",
".wmv",
".mov",
".gif",
];
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
private log: (...msg: any[]) => void = () => {}; private log: (...msg: any[]) => void = () => {};
@ -260,14 +268,7 @@ export class VertdConverter extends Converter {
}); });
// const res = await fetch(url).then((res) => res.blob()); // const res = await fetch(url).then((res) => res.blob());
const res = await downloadFile(url, input); const res = await downloadFile(url, input);
resolve( resolve(new VertFile(new File([res], input.name), to));
new VertFile(
new File([res], input.name),
to,
this,
undefined,
),
);
break; break;
} }

View File

@ -36,7 +36,7 @@ export class VipsConverter extends Converter {
".heif", // HEIF files that are encoded like HEIC files (and HEIC files in general) aren't supported due to https://github.com/kleisauke/wasm-vips/issues/3 ".heif", // HEIF files that are encoded like HEIC files (and HEIC files in general) aren't supported due to https://github.com/kleisauke/wasm-vips/issues/3
".avif", ".avif",
".jxl", ".jxl",
".svg" ".svg",
]; ];
public readonly reportsProgress = false; public readonly reportsProgress = false;
@ -49,11 +49,17 @@ export class VipsConverter extends Converter {
this.worker.onmessage = (e) => { this.worker.onmessage = (e) => {
const message: WorkerMessage = e.data; const message: WorkerMessage = e.data;
log(["converters", this.name], `received message ${message.type}`); log(["converters", this.name], `received message ${message.type}`);
if (message.type === "loaded") { if (message.type === "loaded") {
this.ready = true; this.ready = true;
} else if (message.type === "error") { } else if (message.type === "error") {
error(["converters", this.name], `error in worker: ${message.error}`); error(
addToast("error", `Error in VIPS worker, some features may not work.`); ["converters", this.name],
`error in worker: ${message.error}`,
);
addToast(
"error",
`Error in VIPS worker, some features may not work.`,
);
throw new Error(message.error); throw new Error(message.error);
} }
}; };
@ -78,7 +84,7 @@ export class VipsConverter extends Converter {
window.plausible("convert", { window.plausible("convert", {
props: { props: {
type: "image", type: "image",
} },
}); });
return new VertFile( return new VertFile(
new File([res.output as unknown as BlobPart], input.name), new File([res.output as unknown as BlobPart], input.name),

View File

@ -9,7 +9,7 @@ class Files {
public files = $state<VertFile[]>([]); public files = $state<VertFile[]>([]);
public requiredConverters = $derived( public requiredConverters = $derived(
Array.from(new Set(files.files.map((f) => f.converter))), Array.from(new Set(files.files.map((f) => f.converters).flat())),
); );
public ready = $derived( public ready = $derived(
@ -33,15 +33,14 @@ class Files {
try { try {
if (isAudio) { if (isAudio) {
// try to get the thumbnail from the audio via music-metadata // try to get the thumbnail from the audio via music-metadata
const {common} = await parseBlob(file.file, {skipPostHeaders: true}); const { common } = await parseBlob(file.file, {
skipPostHeaders: true,
});
const cover = selectCover(common.picture); const cover = selectCover(common.picture);
if (cover) { if (cover) {
const blob = new Blob( const blob = new Blob([cover.data], {
[cover.data], type: cover.format,
{ });
type: cover.format,
},
);
file.blobUrl = URL.createObjectURL(blob); file.blobUrl = URL.createObjectURL(blob);
} }
} else if (isVideo) { } else if (isVideo) {
@ -118,7 +117,7 @@ class Files {
); );
if (!converter) { if (!converter) {
log(["files"], `no converter found for ${file.name}`); log(["files"], `no converter found for ${file.name}`);
this.files.push(new VertFile(file, format, null)); this.files.push(new VertFile(file, format));
return; return;
} }
const to = converter.supportedFormats.find((f) => f !== format); const to = converter.supportedFormats.find((f) => f !== format);
@ -126,7 +125,7 @@ class Files {
log(["files"], `no output format found for ${file.name}`); log(["files"], `no output format found for ${file.name}`);
return; return;
} }
const vf = new VertFile(file, to, converter); const vf = new VertFile(file, to);
this.files.push(vf); this.files.push(vf);
this._addThumbnail(vf); this._addThumbnail(vf);
@ -136,13 +135,15 @@ class Files {
if (isVideo && !acceptedExternalWarning && !this._warningShown) { if (isVideo && !acceptedExternalWarning && !this._warningShown) {
this._warningShown = true; this._warningShown = true;
const message = const message =
"Some of your files will be uploaded to an external server to be converted. Do you want to continue?"; "If you choose to convert into a video format, some of your files will be uploaded to an external server to be converted. Do you want to continue?";
const buttons = [ const buttons = [
{ {
text: "No", text: "No",
action: () => { action: () => {
this.files = this.files.filter( this.files = this.files.filter((f) =>
(f) => f.converter?.name !== "vertd", f.converters
.map((c) => c.name)
.includes("vertd"),
); );
this._warningShown = false; this._warningShown = false;
}, },

View File

@ -1,3 +1,4 @@
import { converters } from "$lib/converters";
import type { Converter } from "$lib/converters/converter.svelte"; import type { Converter } from "$lib/converters/converter.svelte";
import { error } from "$lib/logger"; import { error } from "$lib/logger";
import { addToast } from "$lib/store/ToastProvider"; import { addToast } from "$lib/store/ToastProvider";
@ -22,29 +23,49 @@ export class VertFile {
public processing = $state(false); public processing = $state(false);
public converter: Converter | null = null; public converters: Converter[] = [];
public findConverters(supportedFormats: string[] = [this.from]) {
const converter = this.converters.filter((converter) =>
converter.supportedFormats.map((f) => supportedFormats.includes(f)),
);
console.log(this.converters, supportedFormats);
return converter;
}
public findConverter() {
const converter = this.converters.find(
(converter) =>
converter.supportedFormats.includes(this.from) &&
converter.supportedFormats.includes(this.to),
);
return converter;
}
constructor( constructor(
public readonly file: File, public readonly file: File,
to: string, to: string,
converter?: Converter | null,
blobUrl?: string, blobUrl?: string,
) { ) {
this.to = to; this.to = to;
this.converter = converter ?? null; this.converters = converters.filter((c) =>
c.supportedFormats.includes(this.from),
);
this.convert = this.convert.bind(this); this.convert = this.convert.bind(this);
this.download = this.download.bind(this); this.download = this.download.bind(this);
this.blobUrl = blobUrl; this.blobUrl = blobUrl;
} }
public async convert() { public async convert() {
if (!this.converter) throw new Error("No converter found"); if (!this.converters.length) throw new Error("No converters found");
const converter = this.findConverter();
if (!converter) throw new Error("No converter found");
this.result = null; this.result = null;
this.progress = 0; this.progress = 0;
this.processing = true; this.processing = true;
let res; let res;
try { try {
res = await this.converter.convert(this, this.to); res = await converter.convert(this, this.to);
this.result = res; this.result = res;
} catch (err) { } catch (err) {
const castedErr = err as Error; const castedErr = err as Error;

View File

@ -5,6 +5,7 @@
import Panel from "$lib/components/visual/Panel.svelte"; import Panel from "$lib/components/visual/Panel.svelte";
import ProgressBar from "$lib/components/visual/ProgressBar.svelte"; import ProgressBar from "$lib/components/visual/ProgressBar.svelte";
import Tooltip from "$lib/components/visual/Tooltip.svelte"; import Tooltip from "$lib/components/visual/Tooltip.svelte";
import { converters } from "$lib/converters";
import { import {
effects, effects,
files, files,
@ -43,15 +44,15 @@
// Set gradient color depending on the file types // Set gradient color depending on the file types
// TODO: if more file types added, add a "fileType" property to the file object // TODO: if more file types added, add a "fileType" property to the file object
const allAudio = files.files.every( const allAudio = files.files.every(
(file) => file.converter?.name === "ffmpeg", (file) => file.findConverter()?.name === "ffmpeg",
); );
const allImages = files.files.every( const allImages = files.files.every(
(file) => (file) =>
file.converter?.name !== "ffmpeg" && file.findConverter()?.name !== "ffmpeg" &&
file.converter?.name !== "vertd", file.findConverter()?.name !== "vertd",
); );
const allVideos = files.files.every( const allVideos = files.files.every(
(file) => file.converter?.name === "vertd", (file) => file.findConverter()?.name === "vertd",
); );
if (files.files.length === 1 && files.files[0].blobUrl && !allVideos) { if (files.files.length === 1 && files.files[0].blobUrl && !allVideos) {
@ -72,11 +73,21 @@
</script> </script>
{#snippet fileItem(file: VertFile, index: number)} {#snippet fileItem(file: VertFile, index: number)}
{@const isAudio = file.converter?.name === "ffmpeg"} {@const availableConverters = file.findConverters()}
{@const isVideo = file.converter?.name === "vertd"} {@const currentConverter = converters.find(
(c) =>
c.supportedFormats.includes(file.from) &&
c.supportedFormats.includes(file.to),
)}
{@const isAudio = converters
.find((c) => c.name === "ffmpeg")
?.supportedFormats.includes(file.from)}
{@const isVideo = converters
.find((c) => c.name === "vertd")
?.supportedFormats.includes(file.from)}
<Panel class="p-5 flex flex-col min-w-0 gap-4 relative"> <Panel class="p-5 flex flex-col min-w-0 gap-4 relative">
<div class="flex-shrink-0 h-8 w-full flex items-center gap-2"> <div class="flex-shrink-0 h-8 w-full flex items-center gap-2">
{#if !file.converter} {#if !converters.length}
<FileQuestionIcon size="24" class="flex-shrink-0" /> <FileQuestionIcon size="24" class="flex-shrink-0" />
{:else if isAudio} {:else if isAudio}
<AudioLines size="24" class="flex-shrink-0" /> <AudioLines size="24" class="flex-shrink-0" />
@ -90,7 +101,7 @@
<ProgressBar <ProgressBar
min={0} min={0}
max={100} max={100}
progress={file.converter?.reportsProgress progress={currentConverter?.reportsProgress
? file.progress ? file.progress
: null} : null}
/> />
@ -110,7 +121,7 @@
<XIcon size="24" class="text-muted" /> <XIcon size="24" class="text-muted" />
</button> </button>
</div> </div>
{#if !file.converter} {#if !currentConverter}
{#if file.name.startsWith("vertd")} {#if file.name.startsWith("vertd")}
<div <div
class="h-full flex flex-col text-center justify-center text-failure" class="h-full flex flex-col text-center justify-center text-failure"
@ -185,10 +196,12 @@
> >
<!-- cannot convert to svg or heif --> <!-- cannot convert to svg or heif -->
<Dropdown <Dropdown
options={file.converter?.supportedFormats?.filter( options={availableConverters
(format) => .flatMap((c) => c.supportedFormats)
format !== ".svg" && format !== ".heif", .filter(
) || []} (format) =>
format !== ".svg" && format !== ".heif",
) || []}
bind:selected={file.to} bind:selected={file.to}
onselect={(option) => handleSelect(option, file)} onselect={(option) => handleSelect(option, file)}
/> />