feat: metadata setting

need to update vertd next

optimizes metadata code too (oops)
This commit is contained in:
Maya 2025-09-07 06:20:30 +03:00
parent c516bf636b
commit 8e68f023d4
No known key found for this signature in database
7 changed files with 85 additions and 64 deletions

View File

@ -77,7 +77,7 @@
"vertd_not_found": "Could not find the vertd instance to start video conversion. Are you sure the instance URL is set correctly?",
"worker_downloading": "The {type} converter is currently being initialized, please wait a few moments.",
"worker_error": "The {type} converter had an error during initialization, please try again later.",
"worker_timeout": "The {type} converter is taking longer than expected to initialize, please wait a few moments.",
"worker_timeout": "The {type} converter is taking longer than expected to initialize, please wait a few more moments or refresh the page.",
"audio": "audio",
"doc": "document",
"image": "image"
@ -104,6 +104,10 @@
"filename_format": "File name format",
"filename_description": "This will determine the name of the file on download, <b>not including the file extension.</b> You can put these following templates in the format, which will be replaced with the relevant information: <b>%name%</b> for the original file name, <b>%extension%</b> for the original file extension, and <b>%date%</b> for a date string of when the file was converted.",
"placeholder": "VERT_%name%",
"metadata": "File metadata",
"metadata_description": "This changes whether any metadata (EXIF, song info, etc.) on the original file is preserved in converted files.",
"keep": "Keep",
"remove": "Remove",
"quality": "Conversion quality",
"quality_description": "This changes the default output quality of the converted files (in its category). Higher values may result in longer conversion times and file size.",
"quality_video": "This changes the default output quality of the converted video files. Higher values may result in longer conversion times and file size.",

View File

@ -83,9 +83,9 @@ export class FFmpegConverter extends Converter {
(async () => {
const baseURL =
"https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.10/dist/esm";
this.status = "downloading";
await this.ffmpeg.load({
coreURL: `${baseURL}/ffmpeg-core.js`,
wasmURL: `${baseURL}/ffmpeg-core.wasm`,
@ -289,8 +289,23 @@ export class FFmpegConverter extends Converter {
const userSampleRate = Settings.instance.settings.ffmpegSampleRate;
const customSampleRate =
Settings.instance.settings.ffmpegCustomSampleRate ?? 44100;
const keepMetadata = Settings.instance.settings.metadata;
let audioBitrateArgs: string[] = [];
let sampleRateArgs: string[] = [];
let metadataArgs: string[] = [];
log(["converters", this.name], `keep metadata: ${keepMetadata}`);
if (!keepMetadata) {
metadataArgs = [
"-map_metadata", // remove metadata
"-1",
"-map_chapters", // remove chapters
"-1",
"-map", // remove cover art
"a",
];
}
const isLosslessToLossy =
lossless.includes(inputFormat) && !lossless.includes(outputFormat);
@ -366,6 +381,7 @@ export class FFmpegConverter extends Converter {
"input",
"-map",
"0:a:0",
...metadataArgs,
...audioBitrateArgs,
...sampleRateArgs,
"output" + to,
@ -379,7 +395,9 @@ export class FFmpegConverter extends Converter {
`Converting audio ${input.from} to video ${to}`,
);
const hasAlbumArt = await this.extractAlbumArt(ffmpeg);
const hasAlbumArt = keepMetadata
? await this.extractAlbumArt(ffmpeg)
: false;
const codecArgs = toArgs(to);
if (hasAlbumArt) {
@ -402,6 +420,7 @@ export class FFmpegConverter extends Converter {
"-r",
"1",
...codecArgs,
...metadataArgs,
...audioBitrateArgs,
...sampleRateArgs,
"output" + to,
@ -421,6 +440,7 @@ export class FFmpegConverter extends Converter {
"-r",
"1",
...codecArgs,
...metadataArgs,
...audioBitrateArgs,
...sampleRateArgs,
"output" + to,
@ -439,6 +459,7 @@ export class FFmpegConverter extends Converter {
"input",
"-c:a",
audioCodec,
...metadataArgs,
...audioBitrateArgs,
...sampleRateArgs,
"output" + to,

View File

@ -139,6 +139,8 @@ export class MagickConverter extends Converter {
}
// every other format handled by magick worker
const keepMetadata: boolean = Settings.instance.settings.metadata ?? true;
log(["converters", this.name], `keep metadata: ${keepMetadata}`);
const msg = {
type: "convert",
input: {
@ -149,6 +151,7 @@ export class MagickConverter extends Converter {
},
to,
compression,
keepMetadata,
} as WorkerMessage;
const res = await this.sendMessage(msg);

View File

@ -1,7 +1,7 @@
<script lang="ts">
import FancyTextInput from "$lib/components/functional/FancyInput.svelte";
import Panel from "$lib/components/visual/Panel.svelte";
import { RefreshCwIcon } from "lucide-svelte";
import { PauseIcon, PlayIcon, RefreshCwIcon } from "lucide-svelte";
import type { ISettings } from "./index.svelte";
import {
CONVERSION_BITRATES,
@ -12,6 +12,7 @@
import { m } from "$lib/paraglide/messages";
import Dropdown from "$lib/components/functional/Dropdown.svelte";
import FancyInput from "$lib/components/functional/FancyInput.svelte";
import { effects } from "$lib/store/index.svelte";
const { settings }: { settings: ISettings } = $props();
</script>
@ -43,6 +44,43 @@
type="text"
/>
</div>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<p class="text-base font-bold">
{m["settings.conversion.metadata"]()}
</p>
<p class="text-sm text-muted font-normal italic">
{m["settings.conversion.metadata_description"]()}
</p>
</div>
<div class="flex flex-col gap-3 w-full">
<div class="flex gap-3 w-full">
<button
onclick={() => (settings.metadata = true)}
class="btn {$effects
? ''
: '!scale-100'} {settings.metadata
? 'selected'
: ''} flex-1 p-4 rounded-lg text-black dynadark:text-white flex items-center justify-center"
>
<PlayIcon size="24" class="inline-block mr-2" />
{m["settings.conversion.keep"]()}
</button>
<button
onclick={() => (settings.metadata = false)}
class="btn {$effects
? ''
: '!scale-100'} {settings.metadata
? ''
: 'selected'} flex-1 p-4 rounded-lg text-black dynadark:text-white flex items-center justify-center"
>
<PauseIcon size="24" class="inline-block mr-2" />
{m["settings.conversion.remove"]()}
</button>
</div>
</div>
</div>
<div class="flex flex-col gap-4">
<div class="flex flex-col gap-2">
<p class="text-base font-bold">
@ -100,7 +138,9 @@
/>
</div>
<div class="flex flex-col gap-2">
<p class="text-sm font-bold select-none">&nbsp;&nbsp;</p>
<p class="text-sm font-bold select-none">
&nbsp;&nbsp;
</p>
<FancyInput
bind:value={
settings.ffmpegCustomSampleRate as unknown as string

View File

@ -7,8 +7,10 @@ export { default as Conversion } from "./Conversion.svelte";
export { default as Vertd } from "./Vertd.svelte";
export { default as Privacy } from "./Privacy.svelte";
// TODO: clean up settings & button code (componetize)
export interface ISettings {
filenameFormat: string;
metadata: boolean;
plausible: boolean;
vertdURL: string;
vertdSpeed: ConversionSpeed; // videos
@ -23,6 +25,7 @@ export class Settings {
public settings: ISettings = $state({
filenameFormat: "VERT_%name%",
metadata: true,
plausible: true,
vertdURL: PUB_VERTD_URL,
vertdSpeed: "slow",

View File

@ -5,6 +5,7 @@ interface ConvertMessage {
input: VertFile;
to: string;
compression: number | null;
keepMetadata?: boolean;
}
interface FinishedMessage {

View File

@ -26,6 +26,7 @@ const handleMessage = async (message: any): Promise<any> => {
switch (message.type) {
case "convert": {
const compression: number | undefined = message.compression;
const keepMetadata: boolean = message.keepMetadata ?? true;
if (!message.to.startsWith(".")) message.to = `.${message.to}`;
message.to = message.to.toLowerCase();
if (message.to === ".jfif") message.to = ".jpeg";
@ -69,6 +70,7 @@ const handleMessage = async (message: any): Promise<any> => {
const output = await magickConvert(
img,
message.to,
keepMetadata,
compression,
);
convertedImgs[i] = output;
@ -111,6 +113,7 @@ const handleMessage = async (message: any): Promise<any> => {
}),
),
message.to,
keepMetadata,
compression,
);
files.push(
@ -161,6 +164,7 @@ const handleMessage = async (message: any): Promise<any> => {
const converted = await magickConvert(
img,
message.to,
keepMetadata,
compression,
);
outputs.push(converted);
@ -225,57 +229,11 @@ const handleMessage = async (message: any): Promise<any> => {
}),
);
// extract metadata
let metadata: Map<string, string> | undefined;
try {
metadata = new Map();
const exifProfile = img.getProfile("exif");
if (exifProfile) {
metadata.set("exif:profile", "true");
}
const iccProfile = img.getProfile("icc");
if (iccProfile) {
metadata.set("icc:profile", "true");
}
const attributeNames = img.attributeNames;
if (attributeNames && attributeNames.length > 0) {
for (const attrName of attributeNames) {
try {
if (
attrName.startsWith("exif:") ||
attrName.startsWith("icc:") ||
attrName.startsWith("date:") ||
attrName.startsWith("tiff:") ||
attrName.startsWith("xmp:") ||
attrName.startsWith("iptc:")
) {
const value = img.getAttribute(attrName);
if (value) {
metadata.set(attrName, value);
}
}
} catch {
// do nothing
}
}
}
console.log(`Parsed ${metadata.size} metadata values`);
if (metadata.size === 0) metadata = undefined;
} catch (e) {
console.warn("Failed to extract metadata:", e);
metadata = undefined;
}
const converted = await magickConvert(
img,
message.to,
keepMetadata,
compression,
metadata,
);
return {
@ -305,8 +263,8 @@ const readToEnd = async (reader: ReadableStreamDefaultReader<Uint8Array>) => {
const magickConvert = async (
img: IMagickImage,
to: string,
keepMetadata: boolean,
compression?: number,
originalMetadata?: Map<string, string>,
) => {
let fmt = to.slice(1).toUpperCase();
if (fmt === "JFIF") fmt = "JPEG";
@ -314,16 +272,7 @@ const magickConvert = async (
const result = await new Promise<Uint8Array>((resolve) => {
// magick-wasm automatically clamps (https://github.com/dlemstra/magick-wasm/blob/76fc6f2b0c0497d2ddc251bbf6174b4dc92ac3ea/src/magick-image.ts#L2480)
if (compression) img.quality = compression;
if (originalMetadata) {
originalMetadata.forEach((value, key) => {
try {
if (!key.endsWith(":profile")) img.setAttribute(key, value);
} catch (e) {
console.warn(`Failed to set metadata ${key}: ${e}`);
}
});
}
if (!keepMetadata) img.strip();
img.write(fmt as unknown as MagickFormat, (o: Uint8Array) => {
resolve(structuredClone(o));