kv-music/js/taglib.ts
Daniel 895d5dd20f feat(metadata): replace taglib-wasm with @dantheman827/taglib-ts
- feat(taglib): updated audio buffer handling in metadata.js to use Uint8Array.
- feat(taglib): refactored addMetadataToAudio to support return type as Blob or Uint8Array

- feat(taglib): add timeout functionality to metadata functions
  - Introduced `withTimeout` utility function to handle operation timeouts.
  - Updated `addMetadataWithTagLib` to use `withTimeout` for promise resolution.
  - Updated `getMetadataWithTagLib` to use `withTimeout` for promise resolution.
  - Added default timeout parameter to both metadata functions.

- feat(taglib): improve metadata handling with ChunkedByteVectorStream
  - Enhanced metadata handling in taglib.ts and taglib.worker.ts to utilize ChunkedByteVectorStream.

- fix(taglib): handle metadata addition failure gracefully
  - Updated `addMetadataWithTagLib` to catch errors and return original audio data if metadata addition fails.

fix(downloads): return original blob if metadata addition fails
 - Wrap addMetadataToAudio call in try-catch to handle errors.

feat(taglib): add direct calling of taglib methods
  - Introduced `direct` parameter to `addMetadataWithTagLib` and `getMetadataWithTagLib` functions for direct processing in the current thread.
  - Exported taglib worker functions.
2026-03-19 15:14:52 -05:00

190 lines
6.9 KiB
TypeScript

import { doTimed, doTimedAsync } from './doTimed';
import type {
AddMetadataMessage,
TagLibFileResponse,
TagLibMetadataResponse,
TagLibReadMetadata,
TagLibReadTypes,
TagLibWriteTypes,
} from './taglib.types';
import TagLibWorker from './taglib.worker?worker';
export async function withTimeout<T>(callback: () => Promise<T>, timeout: number): Promise<T> {
return new Promise<T>((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`Operation timed out after ${timeout} ms`));
}, timeout);
callback()
.then((result) => {
clearTimeout(timer);
resolve(result);
})
.catch((err) => {
clearTimeout(timer);
reject(err);
});
});
}
function toUint8Array(audioData: ArrayBufferLike | Uint8Array) {
if (audioData instanceof Uint8Array) {
return audioData;
}
return doTimed(
`Converting audio data (${(audioData as any)?.constructor?.name}) to Uint8Array`,
() => new Uint8Array(audioData)
);
}
async function convertInputToTaglib<R = TagLibReadTypes>(
audioData: TagLibReadTypes | TagLibWriteTypes,
direct: boolean = false
): Promise<R> {
if ('FileSystemFileEntry' in globalThis && audioData instanceof FileSystemFileEntry) {
audioData = await doTimedAsync('Getting File from FileSystemFileEntry', async () => {
const file = await new Promise<File>((resolve) =>
(audioData as FileSystemFileEntry).file((f) => resolve(f))
);
return toUint8Array(new Uint8Array(await file.arrayBuffer()));
});
}
if ((audioData instanceof Blob || audioData instanceof File) && !direct) {
return (await doTimedAsync(
`Reading ${audioData instanceof File ? 'File' : 'Blob'} as Uint8Array`,
async () => new Uint8Array(await audioData.arrayBuffer())
)) as R;
} else if ('FileSystemFileHandle' in globalThis && audioData instanceof FileSystemFileHandle && !direct) {
return (await doTimedAsync('Reading File from FileSystemHandle as Uint8Array', async () => {
const file = await audioData.getFile();
const arrayBuffer = await file.arrayBuffer();
return await toUint8Array(arrayBuffer);
})) as R;
} else if (
!(audioData instanceof Uint8Array) &&
!(audioData instanceof Blob) &&
!(audioData instanceof File) &&
!('FileSystemFileEntry' in globalThis && audioData instanceof FileSystemFileEntry) &&
!('FileSystemFileHandle' in globalThis && audioData instanceof FileSystemFileHandle)
) {
return toUint8Array(audioData as any) as R;
}
return audioData as R;
}
const workerModule = import('./taglib.worker.js');
export async function addMetadataWithTagLib(
audioData: TagLibWriteTypes,
data: Omit<AddMetadataMessage, 'type' | 'audioData'>,
filename?: string,
direct: boolean = false,
returnBlob: boolean = false,
timeout: number = 10000
) {
audioData = await convertInputToTaglib(audioData, direct);
if (direct) {
const { addMetadataToAudio } = await workerModule;
return await doTimedAsync('Adding metadata with taglib-ts (direct)', () =>
addMetadataToAudio({
...data,
filename,
audioData,
returnType: returnBlob && direct ? 'blob' : 'uint8array',
})
);
} else {
const worker = new TagLibWorker();
try {
return await doTimedAsync(
'Adding metadata with taglib-ts (worker)',
async () =>
await withTimeout(
() =>
new Promise<Uint8Array>((resolve, reject) => {
worker.onmessage = (e: MessageEvent<TagLibFileResponse>) => {
const { data, error } = e.data;
if (error) {
reject(new Error(error));
} else {
resolve(data!);
}
};
worker.onerror = reject;
worker.onmessageerror = reject;
const transferables: Transferable[] = [];
if ((audioData as any)?.buffer instanceof ArrayBuffer) {
transferables.push((audioData as any).buffer);
}
if ((data as any).cover?.data?.buffer instanceof ArrayBuffer) {
transferables.push((data as any).cover.data.buffer);
}
worker.postMessage({ ...data, type: 'Add', audioData, filename }, transferables);
}),
timeout
)
);
} finally {
worker.terminate();
}
}
}
export async function getMetadataWithTagLib(
audioData: TagLibReadTypes,
filename?: string,
direct: boolean = false,
timeout: number = 10000
) {
audioData = await convertInputToTaglib<TagLibReadTypes>(audioData, direct);
if (direct) {
const { getMetadataFromAudio } = await workerModule;
return await doTimedAsync('Getting metadata with taglib-ts (direct)', () =>
getMetadataFromAudio({ filename, audioData })
);
} else {
const worker = new TagLibWorker();
try {
return await doTimedAsync('Getting metadata with taglib-ts (worker)', () =>
withTimeout(
() =>
new Promise<TagLibReadMetadata>((resolve, reject) => {
worker.onmessage = (e: MessageEvent<TagLibMetadataResponse>) => {
const { data, error } = e.data;
if (error) {
reject(new Error(error));
} else {
resolve(data!);
}
};
worker.onerror = reject;
worker.onmessageerror = reject;
const transferables: Transferable[] = [];
if ((audioData as any)?.buffer instanceof ArrayBuffer) {
transferables.push((audioData as any).buffer);
}
worker.postMessage({ type: 'Get', audioData, filename }, transferables);
}),
timeout
)
);
} finally {
worker.terminate();
}
}
}