2021-12-18 13:55:31 +00:00
|
|
|
import { IAudioMetadata } from 'music-metadata-browser';
|
|
|
|
import ID3Writer from 'browser-id3-writer';
|
|
|
|
import MetaFlac from 'metaflac-js';
|
2021-05-23 15:06:21 +00:00
|
|
|
|
2022-11-20 14:30:56 +00:00
|
|
|
export const split_regex = /[ ]?[,;/_、][ ]?/;
|
|
|
|
|
2021-12-18 13:55:31 +00:00
|
|
|
export const FLAC_HEADER = [0x66, 0x4c, 0x61, 0x43];
|
2021-05-23 13:40:43 +00:00
|
|
|
export const MP3_HEADER = [0x49, 0x44, 0x33];
|
2021-12-18 13:55:31 +00:00
|
|
|
export const OGG_HEADER = [0x4f, 0x67, 0x67, 0x53];
|
2021-05-23 13:40:43 +00:00
|
|
|
export const M4A_HEADER = [0x66, 0x74, 0x79, 0x70];
|
2021-12-18 14:38:07 +00:00
|
|
|
//prettier-ignore
|
2021-05-23 13:40:43 +00:00
|
|
|
export const WMA_HEADER = [
|
2021-12-18 14:38:07 +00:00
|
|
|
0x30, 0x26, 0xb2, 0x75, 0x8e, 0x66, 0xcf, 0x11,
|
|
|
|
0xa6, 0xd9, 0x00, 0xaa, 0x00, 0x62, 0xce, 0x6c,
|
2021-12-18 13:55:31 +00:00
|
|
|
];
|
|
|
|
export const WAV_HEADER = [0x52, 0x49, 0x46, 0x46];
|
|
|
|
export const AAC_HEADER = [0xff, 0xf1];
|
|
|
|
export const DFF_HEADER = [0x46, 0x52, 0x4d, 0x38];
|
2021-05-25 04:27:19 +00:00
|
|
|
|
2021-05-23 14:29:34 +00:00
|
|
|
export const AudioMimeType: { [key: string]: string } = {
|
2021-12-18 13:55:31 +00:00
|
|
|
mp3: 'audio/mpeg',
|
|
|
|
flac: 'audio/flac',
|
|
|
|
m4a: 'audio/mp4',
|
|
|
|
ogg: 'audio/ogg',
|
|
|
|
wma: 'audio/x-ms-wma',
|
|
|
|
wav: 'audio/x-wav',
|
|
|
|
dff: 'audio/x-dff',
|
2021-05-23 14:29:34 +00:00
|
|
|
};
|
2021-05-23 13:40:43 +00:00
|
|
|
|
|
|
|
export function BytesHasPrefix(data: Uint8Array, prefix: number[]): boolean {
|
2021-12-18 13:55:31 +00:00
|
|
|
if (prefix.length > data.length) return false;
|
|
|
|
return prefix.every((val, idx) => {
|
|
|
|
return val === data[idx];
|
|
|
|
});
|
2021-05-23 13:40:43 +00:00
|
|
|
}
|
|
|
|
|
2021-12-18 13:55:31 +00:00
|
|
|
export function BytesEqual(a: Uint8Array, b: Uint8Array): boolean {
|
|
|
|
if (a.length !== b.length) return false;
|
|
|
|
return a.every((val, idx) => {
|
|
|
|
return val === b[idx];
|
|
|
|
});
|
2021-12-16 22:07:43 +00:00
|
|
|
}
|
|
|
|
|
2021-12-18 13:55:31 +00:00
|
|
|
export function SniffAudioExt(data: Uint8Array, fallback_ext: string = 'mp3'): string {
|
|
|
|
if (BytesHasPrefix(data, MP3_HEADER)) return 'mp3';
|
|
|
|
if (BytesHasPrefix(data, FLAC_HEADER)) return 'flac';
|
|
|
|
if (BytesHasPrefix(data, OGG_HEADER)) return 'ogg';
|
|
|
|
if (data.length >= 4 + M4A_HEADER.length && BytesHasPrefix(data.slice(4), M4A_HEADER)) return 'm4a';
|
|
|
|
if (BytesHasPrefix(data, WAV_HEADER)) return 'wav';
|
|
|
|
if (BytesHasPrefix(data, WMA_HEADER)) return 'wma';
|
|
|
|
if (BytesHasPrefix(data, AAC_HEADER)) return 'aac';
|
|
|
|
if (BytesHasPrefix(data, DFF_HEADER)) return 'dff';
|
|
|
|
return fallback_ext;
|
2021-05-23 13:40:43 +00:00
|
|
|
}
|
2021-05-23 13:59:33 +00:00
|
|
|
|
|
|
|
export function GetArrayBuffer(obj: Blob): Promise<ArrayBuffer> {
|
2021-12-18 13:55:31 +00:00
|
|
|
if (!!obj.arrayBuffer) return obj.arrayBuffer();
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const reader = new FileReader();
|
|
|
|
reader.onload = (e) => {
|
|
|
|
const rs = e.target?.result;
|
|
|
|
if (!rs) {
|
|
|
|
reject('read file failed');
|
|
|
|
} else {
|
|
|
|
resolve(rs as ArrayBuffer);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
reader.readAsArrayBuffer(obj);
|
|
|
|
});
|
2021-05-23 13:59:33 +00:00
|
|
|
}
|
2021-05-23 15:06:21 +00:00
|
|
|
|
|
|
|
export function GetCoverFromFile(metadata: IAudioMetadata): string {
|
2021-12-18 13:55:31 +00:00
|
|
|
if (metadata.common?.picture && metadata.common.picture.length > 0) {
|
|
|
|
return URL.createObjectURL(
|
|
|
|
new Blob([metadata.common.picture[0].data], { type: metadata.common.picture[0].format }),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return '';
|
2021-05-23 15:06:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export interface IMusicMetaBasic {
|
2021-12-18 13:55:31 +00:00
|
|
|
title: string;
|
|
|
|
artist?: string;
|
2021-05-23 15:06:21 +00:00
|
|
|
}
|
|
|
|
|
2021-12-18 13:55:31 +00:00
|
|
|
export function GetMetaFromFile(
|
|
|
|
filename: string,
|
|
|
|
exist_title?: string,
|
|
|
|
exist_artist?: string,
|
|
|
|
separator = '-',
|
|
|
|
): IMusicMetaBasic {
|
|
|
|
const meta: IMusicMetaBasic = { title: exist_title ?? '', artist: exist_artist };
|
|
|
|
|
|
|
|
const items = filename.split(separator);
|
|
|
|
if (items.length > 1) {
|
2022-11-20 14:30:56 +00:00
|
|
|
if (!meta.artist || meta.artist.split(split_regex).length < items[0].trim().split(split_regex).length) meta.artist = items[0].trim();
|
2021-12-18 13:55:31 +00:00
|
|
|
if (!meta.title) meta.title = items[1].trim();
|
|
|
|
} else if (items.length === 1) {
|
|
|
|
if (!meta.title) meta.title = items[0].trim();
|
|
|
|
}
|
|
|
|
return meta;
|
2021-05-23 15:06:21 +00:00
|
|
|
}
|
2021-05-23 17:30:38 +00:00
|
|
|
|
2021-12-18 13:55:31 +00:00
|
|
|
export async function GetImageFromURL(
|
|
|
|
src: string,
|
|
|
|
): Promise<{ mime: string; buffer: ArrayBuffer; url: string } | undefined> {
|
|
|
|
try {
|
|
|
|
const resp = await fetch(src);
|
|
|
|
const mime = resp.headers.get('Content-Type');
|
|
|
|
if (mime?.startsWith('image/')) {
|
|
|
|
const buffer = await resp.arrayBuffer();
|
|
|
|
const url = URL.createObjectURL(new Blob([buffer], { type: mime }));
|
|
|
|
return { buffer, url, mime };
|
2021-05-23 17:30:38 +00:00
|
|
|
}
|
2021-12-18 13:55:31 +00:00
|
|
|
} catch (e) {
|
|
|
|
console.warn(e);
|
|
|
|
}
|
2021-05-23 17:30:38 +00:00
|
|
|
}
|
2021-05-23 18:55:42 +00:00
|
|
|
|
|
|
|
export interface IMusicMeta {
|
2021-12-18 13:55:31 +00:00
|
|
|
title: string;
|
|
|
|
artists?: string[];
|
|
|
|
album?: string;
|
2022-11-20 14:30:56 +00:00
|
|
|
albumartist?: string;
|
|
|
|
genre?: string[];
|
2021-12-18 13:55:31 +00:00
|
|
|
picture?: ArrayBuffer;
|
|
|
|
picture_desc?: string;
|
2021-05-23 18:55:42 +00:00
|
|
|
}
|
|
|
|
|
2021-05-23 21:04:16 +00:00
|
|
|
export function WriteMetaToMp3(audioData: Buffer, info: IMusicMeta, original: IAudioMetadata) {
|
2021-12-18 13:55:31 +00:00
|
|
|
const writer = new ID3Writer(audioData);
|
|
|
|
|
|
|
|
// reserve original data
|
|
|
|
const frames = original.native['ID3v2.4'] || original.native['ID3v2.3'] || original.native['ID3v2.2'] || [];
|
|
|
|
frames.forEach((frame) => {
|
|
|
|
if (frame.id !== 'TPE1' && frame.id !== 'TIT2' && frame.id !== 'TALB') {
|
|
|
|
try {
|
|
|
|
writer.setFrame(frame.id, frame.value);
|
|
|
|
} catch (e) {}
|
2021-05-23 18:55:42 +00:00
|
|
|
}
|
2021-12-18 13:55:31 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
const old = original.common;
|
|
|
|
writer
|
|
|
|
.setFrame('TPE1', old?.artists || info.artists || [])
|
|
|
|
.setFrame('TIT2', old?.title || info.title)
|
|
|
|
.setFrame('TALB', old?.album || info.album || '');
|
|
|
|
if (info.picture) {
|
|
|
|
writer.setFrame('APIC', {
|
|
|
|
type: 3,
|
|
|
|
data: info.picture,
|
2021-12-25 04:13:26 +00:00
|
|
|
description: info.picture_desc || '',
|
2021-12-18 13:55:31 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
return writer.addTag();
|
2021-05-23 18:55:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export function WriteMetaToFlac(audioData: Buffer, info: IMusicMeta, original: IAudioMetadata) {
|
2021-12-18 13:55:31 +00:00
|
|
|
const writer = new MetaFlac(audioData);
|
|
|
|
const old = original.common;
|
|
|
|
if (!old.title && !old.album && old.artists) {
|
|
|
|
writer.setTag('TITLE=' + info.title);
|
|
|
|
writer.setTag('ALBUM=' + info.album);
|
|
|
|
if (info.artists) {
|
|
|
|
writer.removeTag('ARTIST');
|
|
|
|
info.artists.forEach((artist) => writer.setTag('ARTIST=' + artist));
|
2021-05-23 18:55:42 +00:00
|
|
|
}
|
2021-12-18 13:55:31 +00:00
|
|
|
}
|
2021-05-23 18:55:42 +00:00
|
|
|
|
2021-12-18 13:55:31 +00:00
|
|
|
if (info.picture) {
|
|
|
|
writer.importPictureFromBuffer(Buffer.from(info.picture));
|
|
|
|
}
|
|
|
|
return writer.save();
|
2021-05-23 18:55:42 +00:00
|
|
|
}
|
2021-06-03 04:47:06 +00:00
|
|
|
|
2022-11-20 14:30:56 +00:00
|
|
|
export function RewriteMetaToMp3(audioData: Buffer, info: IMusicMeta, original: IAudioMetadata) {
|
|
|
|
const writer = new ID3Writer(audioData);
|
|
|
|
|
|
|
|
// reserve original data
|
|
|
|
const frames = original.native['ID3v2.4'] || original.native['ID3v2.3'] || original.native['ID3v2.2'] || [];
|
|
|
|
frames.forEach((frame) => {
|
|
|
|
if (frame.id !== 'TPE1'
|
|
|
|
&& frame.id !== 'TIT2'
|
|
|
|
&& frame.id !== 'TALB'
|
|
|
|
&& frame.id !== 'TPE2'
|
|
|
|
&& frame.id !== 'TCON'
|
|
|
|
) {
|
|
|
|
try {
|
|
|
|
writer.setFrame(frame.id, frame.value);
|
|
|
|
} catch (e) {
|
|
|
|
throw new Error('write unknown mp3 frame failed');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
const old = original.common;
|
|
|
|
writer
|
|
|
|
.setFrame('TPE1', info?.artists || old.artists || [])
|
|
|
|
.setFrame('TIT2', info?.title || old.title)
|
|
|
|
.setFrame('TALB', info?.album || old.album || '')
|
|
|
|
.setFrame('TPE2', info?.albumartist || old.albumartist || '')
|
|
|
|
.setFrame('TCON', info?.genre || old.genre || []);
|
|
|
|
if (info.picture) {
|
|
|
|
writer.setFrame('APIC', {
|
|
|
|
type: 3,
|
|
|
|
data: info.picture,
|
|
|
|
description: info.picture_desc || '',
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return writer.addTag();
|
|
|
|
}
|
|
|
|
|
|
|
|
export function RewriteMetaToFlac(audioData: Buffer, info: IMusicMeta, original: IAudioMetadata) {
|
|
|
|
const writer = new MetaFlac(audioData);
|
|
|
|
const old = original.common;
|
|
|
|
if (info.title) {
|
|
|
|
if (old.title) {
|
|
|
|
writer.removeTag('TITLE');
|
|
|
|
}
|
|
|
|
writer.setTag('TITLE=' + info.title);
|
|
|
|
}
|
|
|
|
if (info.album) {
|
|
|
|
if (old.album) {
|
|
|
|
writer.removeTag('ALBUM');
|
|
|
|
}
|
|
|
|
writer.setTag('ALBUM=' + info.album);
|
|
|
|
}
|
|
|
|
if (info.albumartist) {
|
|
|
|
if (old.albumartist) {
|
|
|
|
writer.removeTag('ALBUMARTIST');
|
|
|
|
}
|
|
|
|
writer.setTag('ALBUMARTIST=' + info.albumartist);
|
|
|
|
}
|
|
|
|
if (info.artists) {
|
|
|
|
if (old.artists) {
|
|
|
|
writer.removeTag('ARTIST');
|
|
|
|
}
|
|
|
|
info.artists.forEach((artist) => writer.setTag('ARTIST=' + artist));
|
|
|
|
}
|
|
|
|
if (info.genre) {
|
|
|
|
if (old.genre) {
|
|
|
|
writer.removeTag('GENRE');
|
|
|
|
}
|
|
|
|
info.genre.forEach((singlegenre) => writer.setTag('GENRE=' + singlegenre));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (info.picture) {
|
|
|
|
writer.importPictureFromBuffer(Buffer.from(info.picture));
|
|
|
|
}
|
|
|
|
return writer.save();
|
|
|
|
}
|
|
|
|
|
2021-06-03 04:47:06 +00:00
|
|
|
export function SplitFilename(n: string): { name: string; ext: string } {
|
2021-12-18 13:55:31 +00:00
|
|
|
const pos = n.lastIndexOf('.');
|
|
|
|
return {
|
|
|
|
ext: n.substring(pos + 1).toLowerCase(),
|
|
|
|
name: n.substring(0, pos),
|
|
|
|
};
|
2021-06-03 04:47:06 +00:00
|
|
|
}
|