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?", "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_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_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", "audio": "audio",
"doc": "document", "doc": "document",
"image": "image" "image": "image"
@ -104,6 +104,10 @@
"filename_format": "File name format", "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.", "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%", "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": "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_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.", "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

@ -289,8 +289,23 @@ export class FFmpegConverter extends Converter {
const userSampleRate = Settings.instance.settings.ffmpegSampleRate; const userSampleRate = Settings.instance.settings.ffmpegSampleRate;
const customSampleRate = const customSampleRate =
Settings.instance.settings.ffmpegCustomSampleRate ?? 44100; Settings.instance.settings.ffmpegCustomSampleRate ?? 44100;
const keepMetadata = Settings.instance.settings.metadata;
let audioBitrateArgs: string[] = []; let audioBitrateArgs: string[] = [];
let sampleRateArgs: 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 = const isLosslessToLossy =
lossless.includes(inputFormat) && !lossless.includes(outputFormat); lossless.includes(inputFormat) && !lossless.includes(outputFormat);
@ -366,6 +381,7 @@ export class FFmpegConverter extends Converter {
"input", "input",
"-map", "-map",
"0:a:0", "0:a:0",
...metadataArgs,
...audioBitrateArgs, ...audioBitrateArgs,
...sampleRateArgs, ...sampleRateArgs,
"output" + to, "output" + to,
@ -379,7 +395,9 @@ export class FFmpegConverter extends Converter {
`Converting audio ${input.from} to video ${to}`, `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); const codecArgs = toArgs(to);
if (hasAlbumArt) { if (hasAlbumArt) {
@ -402,6 +420,7 @@ export class FFmpegConverter extends Converter {
"-r", "-r",
"1", "1",
...codecArgs, ...codecArgs,
...metadataArgs,
...audioBitrateArgs, ...audioBitrateArgs,
...sampleRateArgs, ...sampleRateArgs,
"output" + to, "output" + to,
@ -421,6 +440,7 @@ export class FFmpegConverter extends Converter {
"-r", "-r",
"1", "1",
...codecArgs, ...codecArgs,
...metadataArgs,
...audioBitrateArgs, ...audioBitrateArgs,
...sampleRateArgs, ...sampleRateArgs,
"output" + to, "output" + to,
@ -439,6 +459,7 @@ export class FFmpegConverter extends Converter {
"input", "input",
"-c:a", "-c:a",
audioCodec, audioCodec,
...metadataArgs,
...audioBitrateArgs, ...audioBitrateArgs,
...sampleRateArgs, ...sampleRateArgs,
"output" + to, "output" + to,

View File

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

View File

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import FancyTextInput from "$lib/components/functional/FancyInput.svelte"; import FancyTextInput from "$lib/components/functional/FancyInput.svelte";
import Panel from "$lib/components/visual/Panel.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 type { ISettings } from "./index.svelte";
import { import {
CONVERSION_BITRATES, CONVERSION_BITRATES,
@ -12,6 +12,7 @@
import { m } from "$lib/paraglide/messages"; import { m } from "$lib/paraglide/messages";
import Dropdown from "$lib/components/functional/Dropdown.svelte"; import Dropdown from "$lib/components/functional/Dropdown.svelte";
import FancyInput from "$lib/components/functional/FancyInput.svelte"; import FancyInput from "$lib/components/functional/FancyInput.svelte";
import { effects } from "$lib/store/index.svelte";
const { settings }: { settings: ISettings } = $props(); const { settings }: { settings: ISettings } = $props();
</script> </script>
@ -43,6 +44,43 @@
type="text" type="text"
/> />
</div> </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-4">
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<p class="text-base font-bold"> <p class="text-base font-bold">
@ -100,7 +138,9 @@
/> />
</div> </div>
<div class="flex flex-col gap-2"> <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 <FancyInput
bind:value={ bind:value={
settings.ffmpegCustomSampleRate as unknown as string 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 Vertd } from "./Vertd.svelte";
export { default as Privacy } from "./Privacy.svelte"; export { default as Privacy } from "./Privacy.svelte";
// TODO: clean up settings & button code (componetize)
export interface ISettings { export interface ISettings {
filenameFormat: string; filenameFormat: string;
metadata: boolean;
plausible: boolean; plausible: boolean;
vertdURL: string; vertdURL: string;
vertdSpeed: ConversionSpeed; // videos vertdSpeed: ConversionSpeed; // videos
@ -23,6 +25,7 @@ export class Settings {
public settings: ISettings = $state({ public settings: ISettings = $state({
filenameFormat: "VERT_%name%", filenameFormat: "VERT_%name%",
metadata: true,
plausible: true, plausible: true,
vertdURL: PUB_VERTD_URL, vertdURL: PUB_VERTD_URL,
vertdSpeed: "slow", vertdSpeed: "slow",

View File

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

View File

@ -26,6 +26,7 @@ const handleMessage = async (message: any): Promise<any> => {
switch (message.type) { switch (message.type) {
case "convert": { case "convert": {
const compression: number | undefined = message.compression; const compression: number | undefined = message.compression;
const keepMetadata: boolean = message.keepMetadata ?? true;
if (!message.to.startsWith(".")) message.to = `.${message.to}`; if (!message.to.startsWith(".")) message.to = `.${message.to}`;
message.to = message.to.toLowerCase(); message.to = message.to.toLowerCase();
if (message.to === ".jfif") message.to = ".jpeg"; if (message.to === ".jfif") message.to = ".jpeg";
@ -69,6 +70,7 @@ const handleMessage = async (message: any): Promise<any> => {
const output = await magickConvert( const output = await magickConvert(
img, img,
message.to, message.to,
keepMetadata,
compression, compression,
); );
convertedImgs[i] = output; convertedImgs[i] = output;
@ -111,6 +113,7 @@ const handleMessage = async (message: any): Promise<any> => {
}), }),
), ),
message.to, message.to,
keepMetadata,
compression, compression,
); );
files.push( files.push(
@ -161,6 +164,7 @@ const handleMessage = async (message: any): Promise<any> => {
const converted = await magickConvert( const converted = await magickConvert(
img, img,
message.to, message.to,
keepMetadata,
compression, compression,
); );
outputs.push(converted); 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( const converted = await magickConvert(
img, img,
message.to, message.to,
keepMetadata,
compression, compression,
metadata,
); );
return { return {
@ -305,8 +263,8 @@ const readToEnd = async (reader: ReadableStreamDefaultReader<Uint8Array>) => {
const magickConvert = async ( const magickConvert = async (
img: IMagickImage, img: IMagickImage,
to: string, to: string,
keepMetadata: boolean,
compression?: number, compression?: number,
originalMetadata?: Map<string, string>,
) => { ) => {
let fmt = to.slice(1).toUpperCase(); let fmt = to.slice(1).toUpperCase();
if (fmt === "JFIF") fmt = "JPEG"; if (fmt === "JFIF") fmt = "JPEG";
@ -314,16 +272,7 @@ const magickConvert = async (
const result = await new Promise<Uint8Array>((resolve) => { const result = await new Promise<Uint8Array>((resolve) => {
// magick-wasm automatically clamps (https://github.com/dlemstra/magick-wasm/blob/76fc6f2b0c0497d2ddc251bbf6174b4dc92ac3ea/src/magick-image.ts#L2480) // magick-wasm automatically clamps (https://github.com/dlemstra/magick-wasm/blob/76fc6f2b0c0497d2ddc251bbf6174b4dc92ac3ea/src/magick-image.ts#L2480)
if (compression) img.quality = compression; if (compression) img.quality = compression;
if (!keepMetadata) img.strip();
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}`);
}
});
}
img.write(fmt as unknown as MagickFormat, (o: Uint8Array) => { img.write(fmt as unknown as MagickFormat, (o: Uint8Array) => {
resolve(structuredClone(o)); resolve(structuredClone(o));