1
0
forked from um/web

feat(joox): Fetch meta data from API

(cherry picked from commit 4af1a38334cfc51ce64dd509f2dff694f78010f6)
This commit is contained in:
鲁树人 2021-12-23 14:58:24 +00:00
parent 7fac4c60a5
commit 18a8dbfaa4
3 changed files with 191 additions and 33 deletions

View File

@ -23,10 +23,12 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string)
const ext = SniffAudioExt(musicDecoded); const ext = SniffAudioExt(musicDecoded);
const mime = AudioMimeType[ext]; const mime = AudioMimeType[ext];
const songId = raw_filename.match(/^(\d+)\s\[mqms\d*]$/i)?.[1];
const { album, artist, imgUrl, blob, title } = await extractQQMusicMeta( const { album, artist, imgUrl, blob, title } = await extractQQMusicMeta(
new Blob([musicDecoded], { type: mime }), new Blob([musicDecoded], { type: mime }),
raw_filename, raw_filename,
ext, ext,
songId,
); );
return { return {

View File

@ -32,3 +32,83 @@ export async function queryAlbumCover(title: string, artist?: string, album?: st
const resp = await fetch(`${endpoint}?${params.toString()}`); const resp = await fetch(`${endpoint}?${params.toString()}`);
return await resp.json(); 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<T> {
code: number;
ts: number;
start_ts: number;
traceid: string;
req_1: {
code: number;
data: T;
};
}
export async function querySongInfoById(id: string | number): Promise<SongInfoResponse> {
const url = `${IXAREA_API_ENDPOINT}/meta/qq-music-raw/${id}`;
const result: RawQMBatchResponse<SongInfoResponse> = 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}`;
}

View File

@ -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 iconv from 'iconv-lite';
import { import {
@ -9,9 +9,30 @@ import {
WriteMetaToMp3, WriteMetaToMp3,
AudioMimeType, AudioMimeType,
} from '@/decrypt/utils'; } 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); const musicMeta = await metaParseBlob(musicBlob);
for (let metaIdx in musicMeta.native) { for (let metaIdx in musicMeta.native) {
if (!musicMeta.native.hasOwnProperty(metaIdx)) continue; 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); if (id) {
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 { try {
const newMeta = { picture: imageInfo.buffer, title: info.title, artists: info.artist?.split(' _ ') }; return fetchMetadataFromSongId(id, ext, musicMeta, musicBlob);
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) { } catch (e) {
console.warn('Error while appending cover image to file ' + 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 { return {
title: info.title, title: info.title,
artist: info.artist, artist: info.artist || '',
album: musicMeta.common.album, album: musicMeta.common.album || '',
imgUrl: imgUrl, imgUrl: imageURL,
blob: await writeMetaToAudioFile({
title: info.title,
artists: info.artist.split(' _ '),
ext,
imageURL,
musicMeta,
blob: musicBlob, 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> { async function getCoverImage(title: string, artist?: string, album?: string): Promise<string> {
const song_query_url = 'https://stats.ixarea.com/apis' + '/music/qq-cover';
try { try {
const data = await queryAlbumCover(title, artist, album); const data = await queryAlbumCover(title, artist, album);
return `${song_query_url}/${data.Type}/${data.Id}`; return getQMImageURLFromPMID(data.Id, data.Type);
} catch (e) { } catch (e) {
console.warn(e); console.warn(e);
} }
return ''; 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;
}