mirror of https://github.com/VERT-sh/VERT.git
feat: metadata setting
need to update vertd next optimizes metadata code too (oops)
This commit is contained in:
parent
c516bf636b
commit
8e68f023d4
|
|
@ -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.",
|
||||||
|
|
|
||||||
|
|
@ -83,9 +83,9 @@ export class FFmpegConverter extends Converter {
|
||||||
(async () => {
|
(async () => {
|
||||||
const baseURL =
|
const baseURL =
|
||||||
"https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.10/dist/esm";
|
"https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.10/dist/esm";
|
||||||
|
|
||||||
this.status = "downloading";
|
this.status = "downloading";
|
||||||
|
|
||||||
await this.ffmpeg.load({
|
await this.ffmpeg.load({
|
||||||
coreURL: `${baseURL}/ffmpeg-core.js`,
|
coreURL: `${baseURL}/ffmpeg-core.js`,
|
||||||
wasmURL: `${baseURL}/ffmpeg-core.wasm`,
|
wasmURL: `${baseURL}/ffmpeg-core.wasm`,
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"> </p>
|
<p class="text-sm font-bold select-none">
|
||||||
|
|
||||||
|
</p>
|
||||||
<FancyInput
|
<FancyInput
|
||||||
bind:value={
|
bind:value={
|
||||||
settings.ffmpegCustomSampleRate as unknown as string
|
settings.ffmpegCustomSampleRate as unknown as string
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue