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.
This commit is contained in:
Daniel 2026-03-19 13:57:54 -05:00
parent 1ba27308d1
commit 895d5dd20f
11 changed files with 513 additions and 386 deletions

0
.gitmodules vendored Normal file
View file

View file

@ -5,6 +5,7 @@
"": { "": {
"name": "monochrome", "name": "monochrome",
"dependencies": { "dependencies": {
"@dantheman827/taglib-ts": "https://github.com/DanTheMan827/taglib-ts/archive/b4238b2627aceb97f58813258046f1259f68cab7.tar.gz",
"@ffmpeg/core": "^0.12.10", "@ffmpeg/core": "^0.12.10",
"@ffmpeg/ffmpeg": "^0.12.15", "@ffmpeg/ffmpeg": "^0.12.15",
"@ffmpeg/util": "^0.12.2", "@ffmpeg/util": "^0.12.2",
@ -23,7 +24,6 @@
"mime": "^4.1.0", "mime": "^4.1.0",
"npm": "^11.11.1", "npm": "^11.11.1",
"pocketbase": "^0.26.8", "pocketbase": "^0.26.8",
"taglib-wasm": "^1.0.5",
"uuid": "^13.0.0", "uuid": "^13.0.0",
}, },
"devDependencies": { "devDependencies": {
@ -260,6 +260,8 @@
"@csstools/selector-specificity": ["@csstools/selector-specificity@5.0.0", "", { "peerDependencies": { "postcss-selector-parser": "^7.0.0" } }, "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw=="], "@csstools/selector-specificity": ["@csstools/selector-specificity@5.0.0", "", { "peerDependencies": { "postcss-selector-parser": "^7.0.0" } }, "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw=="],
"@dantheman827/taglib-ts": ["@dantheman827/taglib-ts@https://github.com/DanTheMan827/taglib-ts/archive/b4238b2627aceb97f58813258046f1259f68cab7.tar.gz", {}, "sha512-rvQOn9GDEj2sH4yV6oUTMMG9+rJbFG7tQkiP6/bhGJARg1Vmdy283j4YFCl+ubkqsMQ+UfAhEWSw5d5lfPVfwQ=="],
"@dual-bundle/import-meta-resolve": ["@dual-bundle/import-meta-resolve@4.2.1", "", {}, "sha512-id+7YRUgoUX6CgV0DtuhirQWodeeA7Lf4i2x71JS/vtA5pRb/hIGWlw+G6MeXvsM+MXrz0VAydTGElX1rAfgPg=="], "@dual-bundle/import-meta-resolve": ["@dual-bundle/import-meta-resolve@4.2.1", "", {}, "sha512-id+7YRUgoUX6CgV0DtuhirQWodeeA7Lf4i2x71JS/vtA5pRb/hIGWlw+G6MeXvsM+MXrz0VAydTGElX1rAfgPg=="],
"@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="], "@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="],
@ -426,8 +428,6 @@
"@lit/reactive-element": ["@lit/reactive-element@2.1.2", "", { "dependencies": { "@lit-labs/ssr-dom-shim": "^1.5.0" } }, "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A=="], "@lit/reactive-element": ["@lit/reactive-element@2.1.2", "", { "dependencies": { "@lit-labs/ssr-dom-shim": "^1.5.0" } }, "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A=="],
"@msgpack/msgpack": ["@msgpack/msgpack@3.1.3", "", {}, "sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA=="],
"@neutralinojs/lib": ["@neutralinojs/lib@6.5.0", "", { "optionalDependencies": { "@rollup/rollup-darwin-x64": "*", "@rollup/rollup-linux-x64-gnu": "*" } }, "sha512-ECgYh+CXAfMR1JVTvDw/kHhjL6LzNNcjk8Va1DZUSBkUwROqFTQ7zseFeuFtwGvutqvlWiwpGmU3s11rg/bdvA=="], "@neutralinojs/lib": ["@neutralinojs/lib@6.5.0", "", { "optionalDependencies": { "@rollup/rollup-darwin-x64": "*", "@rollup/rollup-linux-x64-gnu": "*" } }, "sha512-ECgYh+CXAfMR1JVTvDw/kHhjL6LzNNcjk8Va1DZUSBkUwROqFTQ7zseFeuFtwGvutqvlWiwpGmU3s11rg/bdvA=="],
"@neutralinojs/neu": ["@neutralinojs/neu@11.7.0", "", { "dependencies": { "@electron/asar": "^3.0.3", "chalk": "^4.1.0", "chokidar": "^4.0.3", "commander": "^7.2.0", "configstore": "^5.0.1", "edit-json-file": "^1.6.2", "follow-redirects": "^1.13.1", "fs-extra": "^9.0.1", "pe-library": "^1.0.1", "png2icons": "^2.0.1", "postject": "1.0.0-alpha.6", "recursive-readdir": "^2.2.2", "resedit": "^2.0.2", "spawn-command": "^1.0.0", "tcp-port-used": "^1.0.2", "uuid": "^8.3.2", "websocket": "^1.0.35", "zip-lib": "^1.0.4" }, "bin": { "neu": "bin/neu.js" } }, "sha512-fUqvR70a+BpKI9mrD92ldZkVC24Rs8XL/9m7zmOCLgCRys3yuWy7vEsxpHzKMzqTiQJkTYIsLmcR8VMzNIjuSw=="], "@neutralinojs/neu": ["@neutralinojs/neu@11.7.0", "", { "dependencies": { "@electron/asar": "^3.0.3", "chalk": "^4.1.0", "chokidar": "^4.0.3", "commander": "^7.2.0", "configstore": "^5.0.1", "edit-json-file": "^1.6.2", "follow-redirects": "^1.13.1", "fs-extra": "^9.0.1", "pe-library": "^1.0.1", "png2icons": "^2.0.1", "postject": "1.0.0-alpha.6", "recursive-readdir": "^2.2.2", "resedit": "^2.0.2", "spawn-command": "^1.0.0", "tcp-port-used": "^1.0.2", "uuid": "^8.3.2", "websocket": "^1.0.35", "zip-lib": "^1.0.4" }, "bin": { "neu": "bin/neu.js" } }, "sha512-fUqvR70a+BpKI9mrD92ldZkVC24Rs8XL/9m7zmOCLgCRys3yuWy7vEsxpHzKMzqTiQJkTYIsLmcR8VMzNIjuSw=="],
@ -1312,8 +1312,6 @@
"table": ["table@6.9.0", "", { "dependencies": { "ajv": "^8.0.1", "lodash.truncate": "^4.4.2", "slice-ansi": "^4.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1" } }, "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A=="], "table": ["table@6.9.0", "", { "dependencies": { "ajv": "^8.0.1", "lodash.truncate": "^4.4.2", "slice-ansi": "^4.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1" } }, "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A=="],
"taglib-wasm": ["taglib-wasm@1.0.5", "", { "dependencies": { "@msgpack/msgpack": "^3.1.3" }, "peerDependencies": { "typescript": ">=4.5.0" }, "optionalPeers": ["typescript"] }, "sha512-kuDHX78FbjLOqldWxBBkEgjyyDagYRGcYqr4g6ObkmEMO203Sp1R0KxkELoH9VkZxsOVR8yoSWaSaG9f5oTqyQ=="],
"tcp-port-used": ["tcp-port-used@1.0.2", "", { "dependencies": { "debug": "4.3.1", "is2": "^2.0.6" } }, "sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA=="], "tcp-port-used": ["tcp-port-used@1.0.2", "", { "dependencies": { "debug": "4.3.1", "is2": "^2.0.6" } }, "sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA=="],
"temp-dir": ["temp-dir@2.0.0", "", {}, "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg=="], "temp-dir": ["temp-dir@2.0.0", "", {}, "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg=="],

View file

@ -12,7 +12,6 @@ import {
} from './utils.js'; } from './utils.js';
import { trackDateSettings } from './storage.js'; import { trackDateSettings } from './storage.js';
import { APICache } from './cache.js'; import { APICache } from './cache.js';
import { addMetadataToAudio, prefetchMetadataObjects } from './metadata.js';
import { DashDownloader } from './dash-downloader.ts'; import { DashDownloader } from './dash-downloader.ts';
import { HlsDownloader } from './hls-downloader.js'; import { HlsDownloader } from './hls-downloader.js';
import { MP3EncodingError } from './mp3-encoder.js'; import { MP3EncodingError } from './mp3-encoder.js';
@ -1320,6 +1319,8 @@ export class LosslessAPI {
async downloadTrack(id, quality = 'HI_RES_LOSSLESS', filename, options = {}) { async downloadTrack(id, quality = 'HI_RES_LOSSLESS', filename, options = {}) {
// Load ffmpeg in the background. // Load ffmpeg in the background.
loadFfmpeg().catch(console.error); loadFfmpeg().catch(console.error);
const metadataModule = await import('./metadata.js');
const { prefetchMetadataObjects, addMetadataToAudio } = metadataModule;
const { onProgress, track, calculateDashBytes = true } = options; const { onProgress, track, calculateDashBytes = true } = options;
const prefetchPromises = prefetchMetadataObjects(track, this); const prefetchPromises = prefetchMetadataObjects(track, this);
@ -1504,7 +1505,11 @@ export class LosslessAPI {
} }
onProgress?.(new DownloadProgress('Adding metadata')); onProgress?.(new DownloadProgress('Adding metadata'));
try {
blob = await addMetadataToAudio(blob, enrichedTrack, this, quality, prefetchPromises); blob = await addMetadataToAudio(blob, enrichedTrack, this, quality, prefetchPromises);
} catch (err) {
console.error(err);
}
} }
} }

View file

@ -15,7 +15,6 @@ import {
import { AbortError } from './errorTypes.ts'; import { AbortError } from './errorTypes.ts';
import { lyricsSettings, bulkDownloadSettings, playlistSettings } from './storage.js'; import { lyricsSettings, bulkDownloadSettings, playlistSettings } from './storage.js';
import { generateM3U, generateM3U8, generateCUE, generateNFO, generateJSON } from './playlist-generator.js'; import { generateM3U, generateM3U8, generateCUE, generateNFO, generateJSON } from './playlist-generator.js';
import { triggerDownload } from './download-utils.ts';
import { import {
ZipStreamWriter, ZipStreamWriter,
ZipBlobWriter, ZipBlobWriter,

View file

@ -1,13 +1,5 @@
import { import { getCoverBlob, getTrackTitle, getFullArtistString, getMimeType, getTrackCoverId } from './utils.js';
getCoverBlob, import { addMetadataWithTagLib, getMetadataWithTagLib } from './taglib.ts';
getTrackTitle,
getFullArtistString,
getMimeType,
getTrackCoverId,
getTrackDiscNumber,
getExtensionFromBlob,
} from './utils.js';
import { fetchTagLib, addMetadataWithTagLib, getMetadataWithTagLib } from './taglib.ts';
import { doTimed, doTimedAsync } from './doTimed.ts'; import { doTimed, doTimedAsync } from './doTimed.ts';
import { managers } from './app.js'; import { managers } from './app.js';
@ -19,7 +11,6 @@ export const METADATA_STRINGS = {
}; };
export function prefetchMetadataObjects(track, api, coverBlob = null) { export function prefetchMetadataObjects(track, api, coverBlob = null) {
const _tagLib = fetchTagLib().catch(console.error);
const coverId = getTrackCoverId(track); const coverId = getTrackCoverId(track);
const coverFetch = coverBlob const coverFetch = coverBlob
? Promise.resolve(coverBlob) ? Promise.resolve(coverBlob)
@ -28,7 +19,7 @@ export function prefetchMetadataObjects(track, api, coverBlob = null) {
: Promise.resolve(null); : Promise.resolve(null);
const lyricsFetch = managers?.lyricsManager?.fetchLyrics?.(track.id, track)?.catch(console.error); const lyricsFetch = managers?.lyricsManager?.fetchLyrics?.(track.id, track)?.catch(console.error);
return { _tagLib, coverFetch, lyricsFetch }; return { coverFetch, lyricsFetch };
} }
/** /**
@ -47,8 +38,6 @@ export async function addMetadataToAudio(audioBlob, track, api, _quality, prefet
*/ */
const data = {}; const data = {};
const audioBuffer = await doTimedAsync('Get audio array buffer', () => audioBlob.arrayBuffer());
try { try {
data.title = getTrackTitle(track); data.title = getTrackTitle(track);
data.artist = getFullArtistString(track); data.artist = getFullArtistString(track);
@ -117,16 +106,14 @@ export async function addMetadataToAudio(audioBlob, track, api, _quality, prefet
console.warn('Error setting lyrics metadata', track, e); console.warn('Error setting lyrics metadata', track, e);
} }
const newAudioBuffer = await addMetadataWithTagLib(audioBuffer, { return await addMetadataWithTagLib(
audioBlob,
{
...data, ...data,
}); },
undefined,
return doTimed( true,
'Create new audio blob', true
() =>
new Blob([newAudioBuffer], {
type: audioBlob.type,
})
); );
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@ -137,12 +124,12 @@ export async function addMetadataToAudio(audioBlob, track, api, _quality, prefet
/** /**
* Reads metadata from a file * Reads metadata from a file
* @param {File} file * @param {Uint8Array | Blob | File | FileSystemFileHandle | FileSystemFileEntry} file
* @returns {Promise<Object>} Track metadata * @returns {Promise<Object>} Track metadata
*/ */
export async function readTrackMetadata(file, siblings = []) { export async function readTrackMetadata(file, { filename = file?.name || 'Unknown Title', siblings } = {}) {
const metadata = { const metadata = {
title: file.name.replace(/\.[^/.]+$/, ''), title: filename?.replace(/\.[^/.]+$/, ''),
artists: [], artists: [],
artist: { name: 'Unknown Artist' }, // For fallback/compatibility artist: { name: 'Unknown Artist' }, // For fallback/compatibility
album: { title: 'Unknown Album', cover: 'assets/appicon.png', releaseDate: null }, album: { title: 'Unknown Album', cover: 'assets/appicon.png', releaseDate: null },
@ -152,16 +139,16 @@ export async function readTrackMetadata(file, siblings = []) {
explicit: false, explicit: false,
isLocal: true, isLocal: true,
file: file, file: file,
id: `local-${file.name}-${file.lastModified}`, id: `local-${filename}-${file.lastModified}`,
}; };
try { try {
const data = await getMetadataWithTagLib(await file.arrayBuffer()); const data = await getMetadataWithTagLib(file, filename, true);
if (data) { if (data) {
metadata.title = data.title || metadata.title; metadata.title = data.title || metadata.title;
const artistNames = (data.artist || "") const artistNames = (data.artist || '')
.split(";") .split(';')
.map((a) => a.trim()) .map((a) => a.trim())
.filter((a) => a); .filter((a) => a);
@ -175,7 +162,7 @@ export async function readTrackMetadata(file, siblings = []) {
if (data.albumArtist) { if (data.albumArtist) {
metadata.album.artist = { name: data.albumArtist }; metadata.album.artist = { name: data.albumArtist };
} else if (metadata.artist.name !== "Unknown Artist") { } else if (metadata.artist.name !== 'Unknown Artist') {
metadata.album.artist = { name: metadata.artist.name }; metadata.album.artist = { name: metadata.artist.name };
} }
@ -190,11 +177,11 @@ export async function readTrackMetadata(file, siblings = []) {
metadata.explicit = !!data.explicit; metadata.explicit = !!data.explicit;
} }
} catch (e) { } catch (e) {
console.warn('Error reading metadata for', file.name, e); console.warn('Error reading metadata for', filename, e);
} }
if (metadata.album.cover === 'assets/appicon.png' && siblings.length > 0) { if (metadata.album.cover === 'assets/appicon.png' && siblings.length > 0) {
const baseName = file.name.substring(0, file.name.lastIndexOf('.')); const baseName = filename.substring(0, filename.lastIndexOf('.'));
const imageExtensions = ['.jpg', '.jpeg', '.png', '.webp']; const imageExtensions = ['.jpg', '.jpeg', '.png', '.webp'];
const coverFile = siblings.find((f) => { const coverFile = siblings.find((f) => {
const fName = f.name; const fName = f.name;

View file

@ -1,38 +1,113 @@
import { TagLib } from 'taglib-wasm'; import { doTimed, doTimedAsync } from './doTimed';
import { fetchBlobURL } from './utils';
import _TagLibWasm from '!/taglib-wasm/dist/taglib-web.wasm?blob-url';
import type { import type {
AddMetadataMessage, AddMetadataMessage,
TagLibFileResponse, TagLibFileResponse,
TagLibMetadataResponse, TagLibMetadataResponse,
TagLibReadMetadata, TagLibReadMetadata,
TagLibReadTypes,
TagLibWriteTypes,
} from './taglib.types'; } from './taglib.types';
import TagLibWorker from './taglib.worker?worker'; import TagLibWorker from './taglib.worker?worker';
let tagLib: Promise<TagLib> | null = null; 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);
async function fetchTagLib(): Promise<string> { callback()
return fetchTagLib.blobUrl || (fetchTagLib.blobUrl = await _TagLibWasm()); .then((result) => {
clearTimeout(timer);
resolve(result);
})
.catch((err) => {
clearTimeout(timer);
reject(err);
});
});
} }
namespace fetchTagLib { function toUint8Array(audioData: ArrayBufferLike | Uint8Array) {
export let blobUrl = ''; if (audioData instanceof Uint8Array) {
return audioData;
} }
export { fetchTagLib }; 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( export async function addMetadataWithTagLib(
audioData: Uint8Array, audioData: TagLibWriteTypes,
data: Omit<AddMetadataMessage, 'type' | 'wasmUrl' | 'audioData'> data: Omit<AddMetadataMessage, 'type' | 'audioData'>,
filename?: string,
direct: boolean = false,
returnBlob: boolean = false,
timeout: number = 10000
) { ) {
if (!(audioData instanceof Uint8Array)) { audioData = await convertInputToTaglib(audioData, direct);
audioData = new Uint8Array(audioData);
}
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(); const worker = new TagLibWorker();
const wasmUrl = await fetchTagLib();
return new Promise<Uint8Array>((resolve, reject) => { try {
return await doTimedAsync(
'Adding metadata with taglib-ts (worker)',
async () =>
await withTimeout(
() =>
new Promise<Uint8Array>((resolve, reject) => {
worker.onmessage = (e: MessageEvent<TagLibFileResponse>) => { worker.onmessage = (e: MessageEvent<TagLibFileResponse>) => {
const { data, error } = e.data; const { data, error } = e.data;
@ -45,24 +120,48 @@ export async function addMetadataWithTagLib(
worker.onerror = reject; worker.onerror = reject;
worker.onmessageerror = reject; worker.onmessageerror = reject;
const transferables: Transferable[] = [audioData.buffer]; 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) { if ((data as any).cover?.data?.buffer instanceof ArrayBuffer) {
transferables.push((data as any).cover.data.buffer); transferables.push((data as any).cover.data.buffer);
} }
worker.postMessage({ ...data, type: 'Add', wasmUrl, audioData }, transferables); worker.postMessage({ ...data, type: 'Add', audioData, filename }, transferables);
}); }),
timeout
)
);
} finally {
worker.terminate();
}
}
} }
export async function getMetadataWithTagLib(audioData: Uint8Array) { export async function getMetadataWithTagLib(
if (!(audioData instanceof Uint8Array)) { audioData: TagLibReadTypes,
audioData = new Uint8Array(audioData); 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(); const worker = new TagLibWorker();
const wasmUrl = await fetchTagLib();
return new Promise<TagLibReadMetadata>((resolve, reject) => { try {
return await doTimedAsync('Getting metadata with taglib-ts (worker)', () =>
withTimeout(
() =>
new Promise<TagLibReadMetadata>((resolve, reject) => {
worker.onmessage = (e: MessageEvent<TagLibMetadataResponse>) => { worker.onmessage = (e: MessageEvent<TagLibMetadataResponse>) => {
const { data, error } = e.data; const { data, error } = e.data;
@ -74,6 +173,18 @@ export async function getMetadataWithTagLib(audioData: Uint8Array) {
}; };
worker.onerror = reject; worker.onerror = reject;
worker.onmessageerror = reject; worker.onmessageerror = reject;
worker.postMessage({ type: 'Get', wasmUrl, audioData }, [audioData.buffer]);
}); 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();
}
}
} }

View file

@ -1,9 +1,11 @@
import type { FileRef } from '!/@dantheman827/taglib-ts/src/fileRef';
export type TagLibWorkerMessageType = 'Add' | 'Get'; export type TagLibWorkerMessageType = 'Add' | 'Get';
export interface TagLibWorkerMessage { export interface TagLibWorkerMessage<T = Uint8Array> {
type: TagLibWorkerMessageType; type: TagLibWorkerMessageType;
wasmUrl: string; audioData: T;
audioData: Uint8Array; filename?: string;
} }
export interface TagLibWorkerResponse<T> { export interface TagLibWorkerResponse<T> {
@ -50,6 +52,19 @@ export type AddMetadataMessage = TagLibWorkerMessage & {
type: 'Add'; type: 'Add';
} & TagLibMetadata; } & TagLibMetadata;
export type GetMetadataMessage = TagLibWorkerMessage & { export type GetMetadataMessage = TagLibWorkerMessage<TagLibReadTypes> & {
type: 'Get'; type: 'Get';
}; };
export type TagLibReadTypes = Uint8Array | Blob | File | FileSystemFileHandle | FileSystemFileEntry;
export type TagLibWriteTypes = Uint8Array;
export type _AddMetadataMessage = Omit<AddMetadataMessage, 'audioData' | 'type'> & {
audioRef?: FileRef | null;
audioData?: Uint8Array;
returnType?: 'blob' | 'uint8array';
};
export type _GetMetadataMessage = Omit<GetMetadataMessage, 'audioData' | 'type'> & {
audioRef?: FileRef | null;
audioData?: TagLibReadTypes;
};

View file

@ -1,9 +1,13 @@
// filepath: /workspaces/monochrome/js/taglib.worker.ts // filepath: /workspaces/monochrome/js/taglib.worker.ts
declare var self: DedicatedWorkerGlobalScope; declare var self: DedicatedWorkerGlobalScope;
import { TagLib, type PictureType } from 'taglib-wasm'; import { ByteVector } from '!/@dantheman827/taglib-ts/src/byteVector.js';
import { Mp4Tag, Mp4Item } from '!/@dantheman827/taglib-ts/src/mp4/mp4Tag.js';
import { Variant } from '!/@dantheman827/taglib-ts/src/toolkit/variant.js';
import { doTimed, doTimedAsync } from './doTimed'; import { doTimed, doTimedAsync } from './doTimed';
import type { import type {
_AddMetadataMessage,
_GetMetadataMessage,
AddMetadataMessage, AddMetadataMessage,
GetMetadataMessage, GetMetadataMessage,
TagLibFileResponse, TagLibFileResponse,
@ -13,15 +17,27 @@ import type {
TagLibWorkerMessage, TagLibWorkerMessage,
TagLibWorkerResponse, TagLibWorkerResponse,
} from './taglib.types'; } from './taglib.types';
import { File as TagLibFile } from '!/@dantheman827/taglib-ts/src/file.js';
import { FileRef } from '!/@dantheman827/taglib-ts/src/fileRef.js';
import { ChunkedByteVectorStream } from '!/@dantheman827/taglib-ts/src/toolkit/chunkedByteVectorStream.js';
import { ReadStyle } from '!/@dantheman827/taglib-ts/src/toolkit/types';
import { BlobStream } from '!/@dantheman827/taglib-ts/src/toolkit/blobStream.js';
import { FileSystemFileHandleStream } from '!/@dantheman827/taglib-ts/src/toolkit/fileSystemFileHandleStream.js';
const PICTURE_TYPE_VALUES = { // Imported to ensure support is bundled in this chunk, even if not directly used
FrontCover: 3, import { FlacFile } from '!/@dantheman827/taglib-ts/src/flac/flacFile.js';
}; import { MpegFile } from '!/@dantheman827/taglib-ts/src/mpeg/mpegFile.js';
import { Mp4File } from '!/@dantheman827/taglib-ts/src/mp4/mp4File.js';
import { OggFile } from '!/@dantheman827/taglib-ts/src/ogg/oggFile.js';
import { OggVorbisFile } from '!/@dantheman827/taglib-ts/src/ogg/vorbis/vorbisFile.js';
async function addMetadataToAudio(message: AddMetadataMessage): Promise<Uint8Array> { export const isWorker = typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope;
export async function addMetadataToAudio(message: _AddMetadataMessage): Promise<Uint8Array | Blob> {
const { const {
wasmUrl,
audioData, audioData,
audioRef,
filename,
title, title,
artist, artist,
albumTitle, albumTitle,
@ -38,226 +54,233 @@ async function addMetadataToAudio(message: AddMetadataMessage): Promise<Uint8Arr
isrc, isrc,
explicit, explicit,
lyrics, lyrics,
returnType = 'uint8array',
} = message; } = message;
const file = await doTimedAsync('Open file with taglib', async () => { const ref =
const tagLib = await TagLib.initialize({ audioRef ??
wasmUrl: wasmUrl, (await doTimedAsync(
}); `Opening file (${audioData.constructor.name})`,
return await tagLib.open(audioData); async () => await getFileRefFromAudioData(audioData)
}); ));
if (!ref || !ref.isValid) {
console.warn('taglib-ts: failed to open file');
return audioData;
}
const underlying = ref.file();
const isMp4 = underlying instanceof Mp4File;
const isMpeg = underlying instanceof MpegFile;
const needsCombinedTrackDisc = isMp4 || isMpeg;
try {
doTimed('Tagging file', () => { doTimed('Tagging file', () => {
const isMp4 = file.isMP4(); const props = ref.properties();
const media = file.audioProperties();
const needsCombinedTrackDisc = isMp4 || media.containerFormat.toLowerCase() === 'mp3';
if (title) { if (title) props.replace('TITLE', [title]);
file.setProperty('TITLE', title); if (artist) props.replace('ARTIST', [artist]);
} if (albumTitle) props.replace('ALBUM', [albumTitle]);
if (albumArtist || artist) props.replace('ALBUMARTIST', [albumArtist || artist!]);
if (artist) {
file.setProperty('ARTIST', artist);
}
if (albumTitle) {
file.setProperty('ALBUM', albumTitle);
}
const _albumArtist = albumArtist || artist;
if (_albumArtist) {
file.setProperty('ALBUMARTIST', _albumArtist);
}
if (trackNumber) { if (trackNumber) {
let trackString = String(trackNumber); const trackStr =
needsCombinedTrackDisc && totalTracks ? `${trackNumber}/${totalTracks}` : String(trackNumber);
if (needsCombinedTrackDisc && trackNumber && totalTracks) { props.replace('TRACKNUMBER', [trackStr]);
trackString = `${trackNumber}/${totalTracks}`;
} }
if (needsCombinedTrackDisc) {
file.setProperty('TRACKNUMBER', trackString);
} else {
file.setProperty('TRACKNUMBER', String(trackNumber));
}
}
if (!needsCombinedTrackDisc && totalTracks) { if (!needsCombinedTrackDisc && totalTracks) {
file.setProperty('TRACKTOTAL', String(totalTracks)); props.replace('TRACKTOTAL', [String(totalTracks)]);
} }
if (discNumber) { if (discNumber) {
let discString = String(discNumber); const discStr = needsCombinedTrackDisc && totalDiscs ? `${discNumber}/${totalDiscs}` : String(discNumber);
props.replace('DISCNUMBER', [discStr]);
if (needsCombinedTrackDisc && discNumber && totalDiscs) {
discString = `${discNumber}/${totalDiscs}`;
} }
if (needsCombinedTrackDisc) {
file.setProperty('DISCNUMBER', discString);
} else {
file.setProperty('DISCNUMBER', String(discNumber));
}
}
if (!needsCombinedTrackDisc && totalDiscs) { if (!needsCombinedTrackDisc && totalDiscs) {
file.setProperty('DISCTOTAL', String(totalDiscs)); props.replace('DISCTOTAL', [String(totalDiscs)]);
} }
if (bpm != null && Number.isFinite(bpm)) { if (bpm != null && Number.isFinite(bpm)) {
file.setProperty('BPM', String(Math.round(bpm))); props.replace('BPM', [String(Math.round(bpm))]);
} }
if (replayGain) { if (replayGain) {
const { albumReplayGain, albumPeakAmplitude, trackReplayGain, trackPeakAmplitude } = replayGain; const { albumReplayGain, albumPeakAmplitude, trackReplayGain, trackPeakAmplitude } = replayGain;
if (albumReplayGain) file.setProperty('REPLAYGAIN_ALBUM_GAIN', String(albumReplayGain)); if (albumReplayGain != null) props.replace('REPLAYGAIN_ALBUM_GAIN', [String(albumReplayGain)]);
if (albumPeakAmplitude) file.setProperty('REPLAYGAIN_ALBUM_PEAK', String(albumPeakAmplitude)); if (albumPeakAmplitude != null) props.replace('REPLAYGAIN_ALBUM_PEAK', [String(albumPeakAmplitude)]);
if (trackReplayGain) file.setProperty('REPLAYGAIN_TRACK_GAIN', String(trackReplayGain)); if (trackReplayGain != null) props.replace('REPLAYGAIN_TRACK_GAIN', [String(trackReplayGain)]);
if (trackPeakAmplitude) file.setProperty('REPLAYGAIN_TRACK_PEAK', String(trackPeakAmplitude)); if (trackPeakAmplitude != null) props.replace('REPLAYGAIN_TRACK_PEAK', [String(trackPeakAmplitude)]);
} }
if (releaseDate) { if (releaseDate) {
try { try {
const year = Number(releaseDate.split('-')[0]); const year = Number(releaseDate.split('-')[0]);
if (!isNaN(year)) { if (!isNaN(year)) props.replace('DATE', [String(year)]);
file.setProperty('DATE', String(year));
}
} catch { } catch {
// Invalid date, skip // Invalid date, skip
} }
} }
if (copyright) { if (copyright) props.replace('COPYRIGHT', [copyright]);
file.setProperty('COPYRIGHT', copyright); if (isrc) props.replace('ISRC', [isrc]);
} if (lyrics) props.replace('LYRICS', [lyrics.replace(/\r/g, '').replace(/\n/g, '\r\n')]);
if (isrc) {
file.setProperty('ISRC', isrc);
if (explicit !== undefined) {
if (isMp4) { if (isMp4) {
file.setMP4Item('xid ', `:isrc:${isrc}`); // rtng is a byte item — must be set directly on the Mp4Tag
} const mp4Tag = (underlying as Mp4File).tag() as Mp4Tag;
} mp4Tag.setItem('rtng', Mp4Item.fromByte(explicit ? 1 : 0));
if (explicit) {
if (isMp4) {
file.setMP4Item('rtng', '1');
} else { } else {
file.setProperty('ITUNESADVISORY', '1'); props.replace('ITUNESADVISORY', [explicit ? '1' : '0']);
} }
} }
if (lyrics) { ref.setProperties(props);
file.setProperty('LYRICS', lyrics.replace(/\r/g, '').replace(/\n/g, '\r\n'));
}
if (cover) { if (cover) {
file.setPictures([ const pictureMap = new Map<string, Variant>();
{ pictureMap.set('data', Variant.fromByteVector(ByteVector.fromByteArray(cover.data)));
mimeType: cover.type, pictureMap.set('mimeType', Variant.fromString(cover.type));
data: cover.data, pictureMap.set('pictureType', Variant.fromInt(3)); // FrontCover
type: 'FrontCover', ref.setComplexProperties('PICTURE', [pictureMap]);
description: 'Cover Art',
},
]);
} }
}); });
await doTimedAsync('Saving in-memory buffer', () => file.save()); await doTimedAsync('Saving in-memory buffer', async () => {
await ref.save();
});
return file.getFileBuffer(); const file = ref.file() as TagLibFile;
} catch (err) { if (!file) return audioData;
console.error(err); const stream = file.stream();
} finally {
file.dispose(); if (stream instanceof ChunkedByteVectorStream) {
const data = doTimed(
'Converting saved file to ' + (returnType == 'blob' ? 'Blob' : 'Uint8Array'),
() => stream.data().data
);
if (returnType === 'blob') {
const blob = new Blob([data as BlobPart], { type: 'application/octet-stream' });
return blob;
}
return data;
} else if (stream instanceof BlobStream) {
const blob = doTimed('Converting saved file to ' + (returnType == 'blob' ? 'Blob' : 'Uint8Array'), () =>
stream.toBlob()
);
if (returnType === 'blob') {
return blob;
}
const arrayBuffer = await doTimed('Reading Blob as ArrayBuffer', async () => await blob.arrayBuffer());
return new Uint8Array(arrayBuffer);
} }
console.warn('taglib-ts: unexpected stream type after saving file', stream);
return audioData; return audioData;
} }
async function getMetadataFromAudio(message: GetMetadataMessage): Promise<TagLibReadMetadata> { export async function getMetadataFromAudio(message: _GetMetadataMessage): Promise<TagLibReadMetadata> {
const { wasmUrl, audioData } = message; const { audioData, audioRef, filename } = message;
const data: TagLibReadMetadata = { const data: TagLibReadMetadata = { duration: 0 };
duration: 0,
};
const file = await doTimedAsync('Open file with taglib', async () => { const ref =
const tagLib = await TagLib.initialize({ audioRef ??
wasmUrl: wasmUrl, (await doTimedAsync(
}); `Opening file (${audioData.constructor.name})`,
return await tagLib.open(audioData); async () => await getFileRefFromAudioData(audioData)
}); ));
try { if (!ref || !ref.isValid) return data;
const pictures = file.getPictures();
const isMp4 = file.isMP4();
const media = file.audioProperties();
data.duration = media.duration; const underlying = ref.file();
const isMp4 = underlying instanceof Mp4File;
const ap = ref.audioProperties();
data.title = file.getProperty('TITLE') || undefined; if (ap) data.duration = ap.lengthInSeconds;
data.artist = file.getProperty('ARTIST') || undefined;
data.albumTitle = file.getProperty('ALBUM') || undefined;
data.albumArtist = file.getProperty('ALBUMARTIST') || undefined;
const [trackNumber, trackTotal] = file
.getProperty('TRACKNUMBER')
?.split('/')
.map((t) => Number(t.trim() || 0) || undefined);
data.trackNumber = trackNumber || undefined;
data.totalTracks = trackTotal ? trackTotal : Number(file.getProperty('TRACKTOTAL') || 0) || undefined;
const [discNumber, discTotal] = file const props = ref.properties();
.getProperty('DISCNUMBER')
?.split('/')
.map((t) => Number(t.trim() || 0) || undefined);
data.discNumber = Number(file.getProperty('DISCNUMBER') || 0) || undefined;
data.bpm = Number(file.getProperty('BPM') || 0) || undefined; data.title = props.get('TITLE')?.[0] || undefined;
data.copyright = file.getProperty('COPYRIGHT') || undefined; data.artist = props.get('ARTIST')?.[0] || undefined;
data.lyrics = file.getProperty('LYRICS') || undefined; data.albumTitle = props.get('ALBUM')?.[0] || undefined;
data.releaseDate = file.getProperty('DATE') || undefined; data.albumArtist = props.get('ALBUMARTIST')?.[0] || undefined;
const [replayGainAlbumGain, replayGainAlbumPeak, replayGainTrackGain, replayGainTrackPeak] = [ const trackStr = props.get('TRACKNUMBER')?.[0] ?? '';
file.getProperty('REPLAYGAIN_ALBUM_GAIN'), const [trackNum, trackTotal] = trackStr.split('/').map((t) => Number(t.trim() || 0) || undefined);
file.getProperty('REPLAYGAIN_ALBUM_PEAK'), data.trackNumber = trackNum || undefined;
file.getProperty('REPLAYGAIN_TRACK_GAIN'), data.totalTracks = trackTotal ?? (Number(props.get('TRACKTOTAL')?.[0] || 0) || undefined);
file.getProperty('REPLAYGAIN_TRACK_PEAK'),
]; const discStr = props.get('DISCNUMBER')?.[0] ?? '';
const [discNum, discTotal] = discStr.split('/').map((t) => Number(t.trim() || 0) || undefined);
data.discNumber = discNum || undefined;
if (!data.totalDiscs) {
data.totalDiscs = discTotal ?? (Number(props.get('DISCTOTAL')?.[0] || 0) || undefined);
}
data.bpm = Number(props.get('BPM')?.[0] || 0) || undefined;
data.copyright = props.get('COPYRIGHT')?.[0] || undefined;
data.lyrics = props.get('LYRICS')?.[0] || undefined;
data.releaseDate = props.get('DATE')?.[0] || undefined;
const replayGain: TagLibMetadata['replayGain'] = {}; const replayGain: TagLibMetadata['replayGain'] = {};
if (replayGainAlbumGain) replayGain.albumReplayGain = replayGainAlbumGain; const albumGain = props.get('REPLAYGAIN_ALBUM_GAIN')?.[0];
if (replayGainAlbumPeak) replayGain.albumPeakAmplitude = Number(replayGainAlbumPeak); const albumPeak = props.get('REPLAYGAIN_ALBUM_PEAK')?.[0];
if (replayGainTrackGain) replayGain.trackReplayGain = replayGainTrackGain; const trackGain = props.get('REPLAYGAIN_TRACK_GAIN')?.[0];
if (replayGainTrackPeak) replayGain.trackPeakAmplitude = Number(replayGainTrackPeak); const trackPeak = props.get('REPLAYGAIN_TRACK_PEAK')?.[0];
if (Object.keys(replayGain).length > 0) { if (albumGain) replayGain.albumReplayGain = albumGain;
data.replayGain = replayGain; if (albumPeak) replayGain.albumPeakAmplitude = Number(albumPeak);
if (trackGain) replayGain.trackReplayGain = trackGain;
if (trackPeak) replayGain.trackPeakAmplitude = Number(trackPeak);
if (Object.keys(replayGain).length > 0) data.replayGain = replayGain;
data.isrc = props.get('ISRC')?.[0] || undefined;
if (isMp4) {
const mp4Tag = (underlying as Mp4File).tag() as Mp4Tag;
data.explicit = mp4Tag.item('rtng')?.toByte() === 1;
} else {
data.explicit = props.get('ITUNESADVISORY')?.[0] === '1';
} }
data.isrc = (isMp4 && file.getMP4Item('xid ')?.split(':').at(-1)) || file.getProperty('ISRC') || undefined; const pictures = ref.complexProperties('PICTURE');
data.explicit = (isMp4 && file.getMP4Item('rtng') === '1') || file.getProperty('ITUNESADVISORY') === '1';
if (pictures.length > 0) { if (pictures.length > 0) {
const picture = pictures.filter((p) => p.type === 'FrontCover')[0]; const pic = pictures[0];
if (picture) { const picData = pic.get('data')?.toByteVector();
data.cover = { const mimeType = pic.get('mimeType')?.toString() ?? '';
data: picture.data, if (picData && picData.length > 0) {
type: picture.mimeType, data.cover = { data: picData.data, type: mimeType };
};
} }
} }
} catch (err) {
console.error(err);
} finally {
file.dispose();
}
return data; return data;
} }
async function getFileRefFromAudioData(
audioData: Uint8Array | Blob | File | FileSystemFileHandle | FileSystemFileEntry
): Promise<FileRef | null> {
if (audioData instanceof Blob || audioData instanceof File) {
const stream = new BlobStream(audioData);
return await FileRef.open(stream, true, ReadStyle.Average);
} else if (audioData instanceof FileSystemFileHandle) {
const stream = await FileSystemFileHandleStream.open(audioData, true);
return await FileRef.open(stream, true, ReadStyle.Average);
} else if ('FileSystemFileEntry' in globalThis && audioData instanceof FileSystemFileEntry) {
const file = await new Promise<File>((resolve) => audioData.file((f) => resolve(f)));
const stream = new BlobStream(file);
return await FileRef.open(stream, true, ReadStyle.Average);
} else if (audioData instanceof Uint8Array) {
const stream = new ChunkedByteVectorStream(audioData);
return await FileRef.open(stream, true, ReadStyle.Average);
}
throw new Error('Unsupported audio data type');
}
if (isWorker) {
self.onmessage = async (event: MessageEvent<TagLibWorkerMessage>) => { self.onmessage = async (event: MessageEvent<TagLibWorkerMessage>) => {
const transfer: Transferable[] = [event.data.audioData.buffer]; const transfer: Transferable[] = [];
if (event.data.audioData?.buffer instanceof ArrayBuffer) {
transfer.push(event.data.audioData.buffer);
}
switch (event.data.type) { switch (event.data.type) {
case 'Add': case 'Add':
@ -266,8 +289,14 @@ self.onmessage = async (event: MessageEvent<TagLibWorkerMessage>) => {
} }
try { try {
const result = await addMetadataToAudio(event.data as AddMetadataMessage); const result = (await addMetadataToAudio({
...event.data,
returnType: 'uint8array',
} as _AddMetadataMessage)) as Uint8Array;
if (result.buffer !== event.data.audioData.buffer) {
transfer.push(result.buffer); transfer.push(result.buffer);
}
self.postMessage( self.postMessage(
{ {
@ -289,7 +318,9 @@ self.onmessage = async (event: MessageEvent<TagLibWorkerMessage>) => {
case 'Get': case 'Get':
try { try {
const result = await getMetadataFromAudio(event.data as GetMetadataMessage); const result = await getMetadataFromAudio({
...event.data,
} as _GetMetadataMessage);
self.postMessage( self.postMessage(
{ {
type: event.data.type, type: event.data.type,
@ -309,3 +340,4 @@ self.onmessage = async (event: MessageEvent<TagLibWorkerMessage>) => {
break; break;
} }
}; };
}

35
package-lock.json generated
View file

@ -9,6 +9,7 @@
"version": "2.5.0", "version": "2.5.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@dantheman827/taglib-ts": "https://github.com/DanTheMan827/taglib-ts/archive/b4238b2627aceb97f58813258046f1259f68cab7.tar.gz",
"@ffmpeg/core": "^0.12.10", "@ffmpeg/core": "^0.12.10",
"@ffmpeg/ffmpeg": "^0.12.15", "@ffmpeg/ffmpeg": "^0.12.15",
"@ffmpeg/util": "^0.12.2", "@ffmpeg/util": "^0.12.2",
@ -27,7 +28,6 @@
"mime": "^4.1.0", "mime": "^4.1.0",
"npm": "^11.11.1", "npm": "^11.11.1",
"pocketbase": "^0.26.8", "pocketbase": "^0.26.8",
"taglib-wasm": "^1.0.5",
"uuid": "^13.0.0" "uuid": "^13.0.0"
}, },
"devDependencies": { "devDependencies": {
@ -1676,6 +1676,12 @@
"postcss-selector-parser": "^7.0.0" "postcss-selector-parser": "^7.0.0"
} }
}, },
"node_modules/@dantheman827/taglib-ts": {
"version": "0.1.4",
"resolved": "https://github.com/DanTheMan827/taglib-ts/archive/b4238b2627aceb97f58813258046f1259f68cab7.tar.gz",
"integrity": "sha512-rvQOn9GDEj2sH4yV6oUTMMG9+rJbFG7tQkiP6/bhGJARg1Vmdy283j4YFCl+ubkqsMQ+UfAhEWSw5d5lfPVfwQ==",
"license": "LGPL-2.1-or-later"
},
"node_modules/@dual-bundle/import-meta-resolve": { "node_modules/@dual-bundle/import-meta-resolve": {
"version": "4.2.1", "version": "4.2.1",
"dev": true, "dev": true,
@ -3012,13 +3018,6 @@
"@lit-labs/ssr-dom-shim": "^1.5.0" "@lit-labs/ssr-dom-shim": "^1.5.0"
} }
}, },
"node_modules/@msgpack/msgpack": {
"version": "3.1.3",
"license": "ISC",
"engines": {
"node": ">= 18"
}
},
"node_modules/@neutralinojs/lib": { "node_modules/@neutralinojs/lib": {
"version": "6.5.0", "version": "6.5.0",
"license": "MIT", "license": "MIT",
@ -10307,24 +10306,6 @@
"node": ">=10.0.0" "node": ">=10.0.0"
} }
}, },
"node_modules/taglib-wasm": {
"version": "1.0.5",
"license": "MIT",
"dependencies": {
"@msgpack/msgpack": "^3.1.3"
},
"engines": {
"node": ">=22.6.0"
},
"peerDependencies": {
"typescript": ">=4.5.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/tcp-port-used": { "node_modules/tcp-port-used": {
"version": "1.0.2", "version": "1.0.2",
"dev": true, "dev": true,
@ -10550,7 +10531,7 @@
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.9.3", "version": "5.9.3",
"devOptional": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",

View file

@ -51,6 +51,7 @@
"serialize-javascript": "^7.0.3" "serialize-javascript": "^7.0.3"
}, },
"dependencies": { "dependencies": {
"@dantheman827/taglib-ts": "https://github.com/DanTheMan827/taglib-ts/archive/b4238b2627aceb97f58813258046f1259f68cab7.tar.gz",
"@ffmpeg/core": "^0.12.10", "@ffmpeg/core": "^0.12.10",
"@ffmpeg/ffmpeg": "^0.12.15", "@ffmpeg/ffmpeg": "^0.12.15",
"@ffmpeg/util": "^0.12.2", "@ffmpeg/util": "^0.12.2",
@ -69,7 +70,6 @@
"mime": "^4.1.0", "mime": "^4.1.0",
"npm": "^11.11.1", "npm": "^11.11.1",
"pocketbase": "^0.26.8", "pocketbase": "^0.26.8",
"taglib-wasm": "^1.0.5",
"uuid": "^13.0.0" "uuid": "^13.0.0"
} }
} }

View file

@ -21,8 +21,7 @@ export default defineConfig(({ mode }) => {
}, },
}, },
optimizeDeps: { optimizeDeps: {
exclude: ['pocketbase', '@ffmpeg/ffmpeg', '@ffmpeg/util', 'taglib-wasm'], exclude: ['pocketbase', '@ffmpeg/ffmpeg', '@ffmpeg/util'],
external: ['taglib-wasm'],
}, },
server: { server: {
fs: { fs: {