kv-music/js/taglib.worker.ts
Daniel 4445cdcd2e
fix(taglib): worker initialization
This was mangled during a merge operation.
2026-03-12 20:47:45 +00:00

311 lines
10 KiB
TypeScript

// filepath: /workspaces/monochrome/js/taglib.worker.ts
declare var self: DedicatedWorkerGlobalScope;
import { TagLib, type PictureType } from 'taglib-wasm';
import { doTimed, doTimedAsync } from './doTimed';
import type {
AddMetadataMessage,
GetMetadataMessage,
TagLibFileResponse,
TagLibMetadata,
TagLibMetadataResponse,
TagLibReadMetadata,
TagLibWorkerMessage,
TagLibWorkerResponse,
} from './taglib.types';
const PICTURE_TYPE_VALUES = {
FrontCover: 3,
};
async function addMetadataToAudio(message: AddMetadataMessage): Promise<Uint8Array> {
const {
wasmUrl,
audioData,
title,
artist,
albumTitle,
albumArtist,
trackNumber,
totalTracks,
discNumber,
totalDiscs,
bpm,
replayGain,
cover,
releaseDate,
copyright,
isrc,
explicit,
lyrics,
} = message;
const file = await doTimedAsync('Open file with taglib', async () => {
const tagLib = await TagLib.initialize({
wasmUrl: wasmUrl,
});
return await tagLib.open(audioData);
});
try {
doTimed('Tagging file', () => {
const isMp4 = file.isMP4();
const media = file.audioProperties();
const needsCombinedTrackDisc = isMp4 || media.containerFormat.toLowerCase() === 'mp3';
if (title) {
file.setProperty('TITLE', title);
}
if (artist) {
file.setProperty('ARTIST', artist);
}
if (albumTitle) {
file.setProperty('ALBUM', albumTitle);
}
const _albumArtist = albumArtist || artist;
if (_albumArtist) {
file.setProperty('ALBUMARTIST', _albumArtist);
}
if (trackNumber) {
let trackString = String(trackNumber);
if (needsCombinedTrackDisc && trackNumber && totalTracks) {
trackString = `${trackNumber}/${totalTracks}`;
}
if (needsCombinedTrackDisc) {
file.setProperty('TRACKNUMBER', trackString);
} else {
file.setProperty('TRACKNUMBER', String(trackNumber));
}
}
if (!needsCombinedTrackDisc && totalTracks) {
file.setProperty('TRACKTOTAL', String(totalTracks));
}
if (discNumber) {
let discString = String(discNumber);
if (needsCombinedTrackDisc && discNumber && totalDiscs) {
discString = `${discNumber}/${totalDiscs}`;
}
if (needsCombinedTrackDisc) {
file.setProperty('DISCNUMBER', discString);
} else {
file.setProperty('DISCNUMBER', String(discNumber));
}
}
if (!needsCombinedTrackDisc && totalDiscs) {
file.setProperty('DISCTOTAL', String(totalDiscs));
}
if (bpm != null && Number.isFinite(bpm)) {
file.setProperty('BPM', String(Math.round(bpm)));
}
if (replayGain) {
const { albumReplayGain, albumPeakAmplitude, trackReplayGain, trackPeakAmplitude } = replayGain;
if (albumReplayGain) file.setProperty('REPLAYGAIN_ALBUM_GAIN', String(albumReplayGain));
if (albumPeakAmplitude) file.setProperty('REPLAYGAIN_ALBUM_PEAK', String(albumPeakAmplitude));
if (trackReplayGain) file.setProperty('REPLAYGAIN_TRACK_GAIN', String(trackReplayGain));
if (trackPeakAmplitude) file.setProperty('REPLAYGAIN_TRACK_PEAK', String(trackPeakAmplitude));
}
if (releaseDate) {
try {
const year = Number(releaseDate.split('-')[0]);
if (!isNaN(year)) {
file.setProperty('DATE', String(year));
}
} catch {
// Invalid date, skip
}
}
if (copyright) {
file.setProperty('COPYRIGHT', copyright);
}
if (isrc) {
file.setProperty('ISRC', isrc);
if (isMp4) {
file.setMP4Item('xid ', `:isrc:${isrc}`);
}
}
if (explicit) {
if (isMp4) {
file.setMP4Item('rtng', '1');
} else {
file.setProperty('ITUNESADVISORY', '1');
}
}
if (lyrics) {
file.setProperty('LYRICS', lyrics.replace(/\r/g, '').replace(/\n/g, '\r\n'));
}
if (cover) {
file.setPictures([
{
mimeType: cover.type,
data: cover.data,
type: 'FrontCover',
description: 'Cover Art',
},
]);
}
});
await doTimedAsync('Saving in-memory buffer', () => file.save());
return file.getFileBuffer();
} catch (err) {
console.error(err);
} finally {
file.dispose();
}
return audioData;
}
async function getMetadataFromAudio(message: GetMetadataMessage): Promise<TagLibReadMetadata> {
const { wasmUrl, audioData } = message;
const data: TagLibReadMetadata = {
duration: 0,
};
const file = await doTimedAsync('Open file with taglib', async () => {
const tagLib = await TagLib.initialize({
wasmUrl: wasmUrl,
});
return await tagLib.open(audioData);
});
try {
const pictures = file.getPictures();
const isMp4 = file.isMP4();
const media = file.audioProperties();
data.duration = media.duration;
data.title = file.getProperty('TITLE') || undefined;
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
.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.copyright = file.getProperty('COPYRIGHT') || undefined;
data.lyrics = file.getProperty('LYRICS') || undefined;
data.releaseDate = file.getProperty('DATE') || undefined;
const [replayGainAlbumGain, replayGainAlbumPeak, replayGainTrackGain, replayGainTrackPeak] = [
file.getProperty('REPLAYGAIN_ALBUM_GAIN'),
file.getProperty('REPLAYGAIN_ALBUM_PEAK'),
file.getProperty('REPLAYGAIN_TRACK_GAIN'),
file.getProperty('REPLAYGAIN_TRACK_PEAK'),
];
const replayGain: TagLibMetadata['replayGain'] = {};
if (replayGainAlbumGain) replayGain.albumReplayGain = replayGainAlbumGain;
if (replayGainAlbumPeak) replayGain.albumPeakAmplitude = Number(replayGainAlbumPeak);
if (replayGainTrackGain) replayGain.trackReplayGain = replayGainTrackGain;
if (replayGainTrackPeak) replayGain.trackPeakAmplitude = Number(replayGainTrackPeak);
if (Object.keys(replayGain).length > 0) {
data.replayGain = replayGain;
}
data.isrc = (isMp4 && file.getMP4Item('xid ')?.split(':').at(-1)) || file.getProperty('ISRC') || undefined;
data.explicit = (isMp4 && file.getMP4Item('rtng') === '1') || file.getProperty('ITUNESADVISORY') === '1';
if (pictures.length > 0) {
const picture = pictures.filter((p) => p.type === 'FrontCover')[0];
if (picture) {
data.cover = {
data: picture.data,
type: picture.mimeType,
};
}
}
} catch (err) {
console.error(err);
} finally {
file.dispose();
}
return data;
}
self.onmessage = async (event: MessageEvent<TagLibWorkerMessage>) => {
const transfer: Transferable[] = [event.data.audioData.buffer];
switch (event.data.type) {
case 'Add':
if ((event.data as AddMetadataMessage).cover?.data?.buffer instanceof ArrayBuffer) {
transfer.push((event.data as AddMetadataMessage).cover.data.buffer);
}
try {
const result = await addMetadataToAudio(event.data as AddMetadataMessage);
transfer.push(result.buffer);
self.postMessage(
{
type: event.data.type,
data: result,
} satisfies TagLibFileResponse,
transfer
);
} catch (error) {
self.postMessage(
{
type: event.data.type,
error: error instanceof Error ? error.message : String(error),
} satisfies TagLibWorkerResponse<undefined>,
transfer
);
}
break;
case 'Get':
try {
const result = await getMetadataFromAudio(event.data as GetMetadataMessage);
self.postMessage(
{
type: event.data.type,
data: result,
} satisfies TagLibMetadataResponse,
transfer
);
} catch (error) {
self.postMessage(
{
type: event.data.type,
error: error instanceof Error ? error.message : String(error),
} satisfies TagLibWorkerResponse<undefined>,
transfer
);
}
break;
}
};