import { IAudioMetadata, parseBlob as metaParseBlob } from 'music-metadata-browser'; import iconv from 'iconv-lite'; import { GetCoverFromFile, GetImageFromURL, GetMetaFromFile, WriteMetaToFlac, WriteMetaToMp3, AudioMimeType, split_regex, } from '@/decrypt/utils'; import { getQMImageURLFromPMID, queryAlbumCover, querySongInfoById } from '@/utils/api'; interface MetaResult { title: string; artist: string; album: string; imgUrl: string; blob: Blob; } const fromGBK = (text?: string) => iconv.decode(new Buffer(text || ''), 'gbk'); /** * * @param musicBlob 音乐文件(解密后) * @param name 文件名 * @param ext 原始后缀名 * @param id 曲目 ID(number类型或纯数字组成的字符串) * @returns Promise */ export async function extractQQMusicMeta( musicBlob: Blob, name: string, ext: string, id?: number | string, ): Promise { const musicMeta = await metaParseBlob(musicBlob); for (let metaIdx in musicMeta.native) { if (!musicMeta.native.hasOwnProperty(metaIdx)) continue; if (musicMeta.native[metaIdx].some((item) => item.id === 'TCON' && item.value === '(12)')) { console.warn('try using gbk encoding to decode meta'); musicMeta.common.artist = ''; if (!musicMeta.common.artists) { musicMeta.common.artist = fromGBK(musicMeta.common.artist); } else { musicMeta.common.artist = musicMeta.common.artists.map(fromGBK).join(); } musicMeta.common.title = fromGBK(musicMeta.common.title); musicMeta.common.album = fromGBK(musicMeta.common.album); } } if (id && id !== '0') { try { return fetchMetadataFromSongId(id, ext, musicMeta, musicBlob); } catch (e) { console.warn('在线获取曲目信息失败,回退到本地 meta 提取', e); } } const info = GetMetaFromFile(name, musicMeta.common.title, musicMeta.common.artist); info.artist = info.artist || ''; let imageURL = GetCoverFromFile(musicMeta); if (!imageURL) { imageURL = await getCoverImage(info.title, info.artist, musicMeta.common.album); } return { title: info.title, artist: info.artist, album: musicMeta.common.album || '', imgUrl: imageURL, blob: await writeMetaToAudioFile({ title: info.title, artists: info.artist.split(split_regex), ext, imageURL, musicMeta, blob: musicBlob, }), }; } async function fetchMetadataFromSongId( id: number | string, ext: string, musicMeta: IAudioMetadata, blob: Blob, ): Promise { const info = await querySongInfoById(id); const imageURL = getQMImageURLFromPMID(info.track_info.album.pmid); const artists = info.track_info.singer.map((singer) => singer.name); return { title: info.track_info.title, artist: artists.join(','), album: info.track_info.album.name, imgUrl: imageURL, blob: await writeMetaToAudioFile({ title: info.track_info.title, artists, ext, imageURL, musicMeta, blob, }), }; } async function getCoverImage(title: string, artist?: string, album?: string): Promise { try { const data = await queryAlbumCover(title, artist, album); return getQMImageURLFromPMID(data.Id, data.Type); } catch (e) { console.warn(e); } return ''; } interface NewAudioMeta { title: string; artists: string[]; ext: string; musicMeta: IAudioMetadata; blob: Blob; imageURL: string; } async function writeMetaToAudioFile(info: NewAudioMeta): Promise { try { const imageInfo = await GetImageFromURL(info.imageURL); if (!imageInfo) { console.warn('获取图像失败'); } const newMeta = { picture: imageInfo?.buffer, title: info.title, artists: info.artists }; const buffer = Buffer.from(await info.blob.arrayBuffer()); const mime = AudioMimeType[info.ext] || AudioMimeType.mp3; if (info.ext === 'mp3') { return new Blob([WriteMetaToMp3(buffer, newMeta, info.musicMeta)], { type: mime }); } else if (info.ext === 'flac') { return new Blob([WriteMetaToFlac(buffer, newMeta, info.musicMeta)], { type: mime }); } else { console.info('writing metadata for ' + info.ext + ' is not being supported for now'); } } catch (e) { console.warn('Error while appending cover image to file ' + e); } return info.blob; }