|
|
@ -1,4 +1,4 @@ |
|
|
|
import { parseBlob as metaParseBlob } from 'music-metadata-browser'; |
|
|
|
import { IAudioMetadata, parseBlob as metaParseBlob } from 'music-metadata-browser'; |
|
|
|
import iconv from 'iconv-lite'; |
|
|
|
|
|
|
|
import { |
|
|
@ -9,9 +9,30 @@ import { |
|
|
|
WriteMetaToMp3, |
|
|
|
AudioMimeType, |
|
|
|
} from '@/decrypt/utils'; |
|
|
|
import { queryAlbumCover } from '@/utils/api'; |
|
|
|
import { getQMImageURLFromPMID, queryAlbumCover, querySongInfoById } from '@/utils/api'; |
|
|
|
|
|
|
|
export async function extractQQMusicMeta(musicBlob: Blob, name: string, ext: string) { |
|
|
|
interface MetaResult { |
|
|
|
title: string; |
|
|
|
artist: string; |
|
|
|
album: string; |
|
|
|
imgUrl: string; |
|
|
|
blob: Blob; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* |
|
|
|
* @param musicBlob 音乐文件(解密后) |
|
|
|
* @param name 文件名 |
|
|
|
* @param ext 原始后缀名 |
|
|
|
* @param id 曲目 ID(<code>number</code>类型或纯数字组成的字符串) |
|
|
|
* @returns Promise |
|
|
|
*/ |
|
|
|
export async function extractQQMusicMeta( |
|
|
|
musicBlob: Blob, |
|
|
|
name: string, |
|
|
|
ext: string, |
|
|
|
id?: number | string, |
|
|
|
): Promise<MetaResult> { |
|
|
|
const musicMeta = await metaParseBlob(musicBlob); |
|
|
|
for (let metaIdx in musicMeta.native) { |
|
|
|
if (!musicMeta.native.hasOwnProperty(metaIdx)) continue; |
|
|
@ -23,49 +44,104 @@ export async function extractQQMusicMeta(musicBlob: Blob, name: string, ext: str |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (id) { |
|
|
|
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 imgUrl = GetCoverFromFile(musicMeta); |
|
|
|
if (!imgUrl) { |
|
|
|
imgUrl = await getCoverImage(info.title, info.artist, musicMeta.common.album); |
|
|
|
if (imgUrl) { |
|
|
|
const imageInfo = await GetImageFromURL(imgUrl); |
|
|
|
if (imageInfo) { |
|
|
|
imgUrl = imageInfo.url; |
|
|
|
try { |
|
|
|
const newMeta = { picture: imageInfo.buffer, title: info.title, artists: info.artist?.split(' _ ') }; |
|
|
|
const buffer = Buffer.from(await musicBlob.arrayBuffer()); |
|
|
|
const mime = AudioMimeType[ext] || AudioMimeType.mp3; |
|
|
|
if (ext === 'mp3') { |
|
|
|
musicBlob = new Blob([WriteMetaToMp3(buffer, newMeta, musicMeta)], { type: mime }); |
|
|
|
} else if (ext === 'flac') { |
|
|
|
musicBlob = new Blob([WriteMetaToFlac(buffer, newMeta, musicMeta)], { type: mime }); |
|
|
|
} else { |
|
|
|
console.info('writing metadata for ' + ext + ' is not being supported for now'); |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
console.warn('Error while appending cover image to file ' + e); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
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: imgUrl, |
|
|
|
blob: musicBlob, |
|
|
|
artist: info.artist || '', |
|
|
|
album: musicMeta.common.album || '', |
|
|
|
imgUrl: imageURL, |
|
|
|
blob: await writeMetaToAudioFile({ |
|
|
|
title: info.title, |
|
|
|
artists: info.artist.split(' _ '), |
|
|
|
ext, |
|
|
|
imageURL, |
|
|
|
musicMeta, |
|
|
|
blob: musicBlob, |
|
|
|
}), |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
async function fetchMetadataFromSongId( |
|
|
|
id: number | string, |
|
|
|
ext: string, |
|
|
|
musicMeta: IAudioMetadata, |
|
|
|
blob: Blob, |
|
|
|
): Promise<MetaResult> { |
|
|
|
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<string> { |
|
|
|
const song_query_url = 'https://stats.ixarea.com/apis' + '/music/qq-cover'; |
|
|
|
try { |
|
|
|
const data = await queryAlbumCover(title, artist, album); |
|
|
|
return `${song_query_url}/${data.Type}/${data.Id}`; |
|
|
|
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<Blob> { |
|
|
|
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; |
|
|
|
} |
|
|
|