From c098b736172816b9fbf159b5c66e9f5ef8ae0f7b Mon Sep 17 00:00:00 2001 From: Jixun Wu Date: Thu, 23 Dec 2021 14:58:24 +0000 Subject: [PATCH] feat(joox): Fetch meta data from API (cherry picked from commit 4af1a38334cfc51ce64dd509f2dff694f78010f6) --- src/decrypt/joox.ts | 2 + src/utils/api.ts | 80 ++++++++++++++++++++++++ src/utils/qm_meta.ts | 142 +++++++++++++++++++++++++++++++++---------- 3 files changed, 191 insertions(+), 33 deletions(-) diff --git a/src/decrypt/joox.ts b/src/decrypt/joox.ts index ce38a37..1cbfcd1 100644 --- a/src/decrypt/joox.ts +++ b/src/decrypt/joox.ts @@ -23,10 +23,12 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string) const ext = SniffAudioExt(musicDecoded); const mime = AudioMimeType[ext]; + const songId = raw_filename.match(/^(\d+)\s\[mqms\d*]$/i)?.[1]; const { album, artist, imgUrl, blob, title } = await extractQQMusicMeta( new Blob([musicDecoded], { type: mime }), raw_filename, ext, + songId, ); return { diff --git a/src/utils/api.ts b/src/utils/api.ts index 331d832..937d487 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -32,3 +32,83 @@ export async function queryAlbumCover(title: string, artist?: string, album?: st const resp = await fetch(`${endpoint}?${params.toString()}`); return await resp.json(); } + +export interface TrackInfo { + id: number; + type: number; + mid: string; + name: string; + title: string; + subtitle: string; + singer: { + id: number; + mid: string; + name: string; + title: string; + type: number; + uin: number; + }[]; + album: { + id: number; + mid: string; + name: string; + title: string; + subtitle: string; + time_public: string; + pmid: string; + }; + interval: number; + index_cd: number; + index_album: number; +} + +export interface SongItemInfo { + title: string; + content: { + value: string; + }[]; +} + +export interface SongInfoResponse { + info: { + company: SongItemInfo; + genre: SongItemInfo; + intro: SongItemInfo; + lan: SongItemInfo; + pub_time: SongItemInfo; + }; + extras: { + name: string; + transname: string; + subtitle: string; + from: string; + wikiurl: string; + }; + track_info: TrackInfo; +} + +export interface RawQMBatchResponse { + code: number; + ts: number; + start_ts: number; + traceid: string; + req_1: { + code: number; + data: T; + }; +} + +export async function querySongInfoById(id: string | number): Promise { + const url = `${IXAREA_API_ENDPOINT}/meta/qq-music-raw/${id}`; + const result: RawQMBatchResponse = await fetch(url).then((r) => r.json()); + if (result.code === 0 && result.req_1.code === 0) { + return result.req_1.data; + } + + throw new Error('请求信息失败'); +} + +const QQ_MUSIC_COVER_URI = 'https://stats.ixarea.com/apis/music/qq-cover'; +export function getQMImageURLFromPMID(pmid: string, type = 1): string { + return `${QQ_MUSIC_COVER_URI}/${type}/${pmid}`; +} diff --git a/src/utils/qm_meta.ts b/src/utils/qm_meta.ts index 3e23f38..b095918 100644 --- a/src/utils/qm_meta.ts +++ b/src/utils/qm_meta.ts @@ -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(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; @@ -23,49 +44,104 @@ export async function extractQQMusicMeta(musicBlob: Blob, name: string, ext: str } } - const info = GetMetaFromFile(name, musicMeta.common.title, musicMeta.common.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); - } - } + 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 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 { + 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 { - 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 { + 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; +}