- 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.
190 lines
6.9 KiB
TypeScript
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();
|
|
}
|
|
}
|
|
}
|