mirror of
https://git.unlock-music.dev/um/web.git
synced 2025-11-05 11:51:24 +00:00
Add Tag Edit Function & Wasm for Qmc & Kgm
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
} from '@/decrypt/utils';
|
||||
import { parseBlob as metaParseBlob } from 'music-metadata-browser';
|
||||
import { DecryptResult } from '@/decrypt/entity';
|
||||
import { DecryptKgmWasm } from '@/decrypt/kgm_wasm';
|
||||
import { decryptKgmByteAtOffsetV2, decryptVprByteAtOffset } from '@jixun/kugou-crypto/dist/utils/decryptionHelper';
|
||||
|
||||
//prettier-ignore
|
||||
@@ -22,31 +23,48 @@ const KgmHeader = [
|
||||
]
|
||||
|
||||
export async function Decrypt(file: File, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
|
||||
const oriData = new Uint8Array(await GetArrayBuffer(file));
|
||||
const oriData = await GetArrayBuffer(file);
|
||||
if (raw_ext === 'vpr') {
|
||||
if (!BytesHasPrefix(oriData, VprHeader)) throw Error('Not a valid vpr file!');
|
||||
if (!BytesHasPrefix(new Uint8Array(oriData), VprHeader)) throw Error('Not a valid vpr file!');
|
||||
} else {
|
||||
if (!BytesHasPrefix(oriData, KgmHeader)) throw Error('Not a valid kgm(a) file!');
|
||||
if (!BytesHasPrefix(new Uint8Array(oriData), KgmHeader)) throw Error('Not a valid kgm(a) file!');
|
||||
}
|
||||
let bHeaderLen = new DataView(oriData.slice(0x10, 0x14).buffer);
|
||||
let headerLen = bHeaderLen.getUint32(0, true);
|
||||
let musicDecoded: Uint8Array | undefined;
|
||||
if (globalThis.WebAssembly) {
|
||||
console.log('kgm: using wasm decoder');
|
||||
|
||||
let audioData = oriData.slice(headerLen);
|
||||
let dataLen = audioData.length;
|
||||
|
||||
let key1 = Array.from(oriData.slice(0x1c, 0x2c));
|
||||
key1.push(0);
|
||||
|
||||
const decryptByte = raw_ext === 'vpr' ? decryptVprByteAtOffset : decryptKgmByteAtOffsetV2;
|
||||
for (let i = 0; i < dataLen; i++) {
|
||||
audioData[i] = decryptByte(audioData[i], key1, i);
|
||||
const kgmDecrypted = await DecryptKgmWasm(oriData, raw_ext);
|
||||
// 若 v2 检测失败,降级到 v1 再尝试一次
|
||||
if (kgmDecrypted.success) {
|
||||
musicDecoded = kgmDecrypted.data;
|
||||
console.log('kgm wasm decoder suceeded');
|
||||
} else {
|
||||
console.warn('KgmWasm failed with error %s', kgmDecrypted.error || '(no error)');
|
||||
}
|
||||
}
|
||||
|
||||
const ext = SniffAudioExt(audioData);
|
||||
if (!musicDecoded) {
|
||||
musicDecoded = new Uint8Array(oriData);
|
||||
let bHeaderLen = new DataView(musicDecoded.slice(0x10, 0x14).buffer);
|
||||
let headerLen = bHeaderLen.getUint32(0, true);
|
||||
|
||||
let key1 = Array.from(musicDecoded.slice(0x1c, 0x2c));
|
||||
key1.push(0);
|
||||
|
||||
musicDecoded = musicDecoded.slice(headerLen);
|
||||
let dataLen = musicDecoded.length;
|
||||
|
||||
const decryptByte = raw_ext === 'vpr' ? decryptVprByteAtOffset : decryptKgmByteAtOffsetV2;
|
||||
for (let i = 0; i < dataLen; i++) {
|
||||
musicDecoded[i] = decryptByte(musicDecoded[i], key1, i);
|
||||
}
|
||||
}
|
||||
|
||||
const ext = SniffAudioExt(musicDecoded);
|
||||
const mime = AudioMimeType[ext];
|
||||
let musicBlob = new Blob([audioData], { type: mime });
|
||||
let musicBlob = new Blob([musicDecoded], { type: mime });
|
||||
const musicMeta = await metaParseBlob(musicBlob);
|
||||
const { title, artist } = GetMetaFromFile(raw_filename, musicMeta.common.title, musicMeta.common.artist);
|
||||
const { title, artist } = GetMetaFromFile(raw_filename, musicMeta.common.title, musicMeta.common.artists == undefined ? musicMeta.common.artist : musicMeta.common.artists.toString());
|
||||
return {
|
||||
album: musicMeta.common.album,
|
||||
picture: GetCoverFromFile(musicMeta),
|
||||
|
||||
67
src/decrypt/kgm_wasm.ts
Normal file
67
src/decrypt/kgm_wasm.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import KgmCryptoModule from '@/KgmWasm/KgmWasmBundle';
|
||||
import { MergeUint8Array } from '@/utils/MergeUint8Array';
|
||||
|
||||
// 每次处理 2M 的数据
|
||||
const DECRYPTION_BUF_SIZE = 2 *1024 * 1024;
|
||||
|
||||
export interface KGMDecryptionResult {
|
||||
success: boolean;
|
||||
data: Uint8Array;
|
||||
error: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密一个 KGM 加密的文件。
|
||||
*
|
||||
* 如果检测并解密成功,返回解密后的 Uint8Array 数据。
|
||||
* @param {ArrayBuffer} kgmBlob 读入的文件 Blob
|
||||
*/
|
||||
export async function DecryptKgmWasm(kgmBlob: ArrayBuffer, ext: string): Promise<KGMDecryptionResult> {
|
||||
const result: KGMDecryptionResult = { success: false, data: new Uint8Array(), error: '' };
|
||||
|
||||
// 初始化模组
|
||||
let KgmCrypto: any;
|
||||
|
||||
try {
|
||||
KgmCrypto = await KgmCryptoModule();
|
||||
} catch (err: any) {
|
||||
result.error = err?.message || 'wasm 加载失败';
|
||||
return result;
|
||||
}
|
||||
if (!KgmCrypto) {
|
||||
result.error = 'wasm 加载失败';
|
||||
return result;
|
||||
}
|
||||
|
||||
// 申请内存块,并文件末端数据到 WASM 的内存堆
|
||||
let kgmBuf = new Uint8Array(kgmBlob);
|
||||
const pQmcBuf = KgmCrypto._malloc(DECRYPTION_BUF_SIZE);
|
||||
KgmCrypto.writeArrayToMemory(kgmBuf.slice(0, DECRYPTION_BUF_SIZE), pQmcBuf);
|
||||
|
||||
// 进行解密初始化
|
||||
const headerSize = KgmCrypto.preDec(pQmcBuf, DECRYPTION_BUF_SIZE, ext);
|
||||
console.log(headerSize);
|
||||
kgmBuf = kgmBuf.slice(headerSize);
|
||||
|
||||
const decryptedParts = [];
|
||||
let offset = 0;
|
||||
let bytesToDecrypt = kgmBuf.length;
|
||||
while (bytesToDecrypt > 0) {
|
||||
const blockSize = Math.min(bytesToDecrypt, DECRYPTION_BUF_SIZE);
|
||||
|
||||
// 解密一些片段
|
||||
const blockData = new Uint8Array(kgmBuf.slice(offset, offset + blockSize));
|
||||
KgmCrypto.writeArrayToMemory(blockData, pQmcBuf);
|
||||
KgmCrypto.decBlob(pQmcBuf, blockSize, offset);
|
||||
decryptedParts.push(KgmCrypto.HEAPU8.slice(pQmcBuf, pQmcBuf + blockSize));
|
||||
|
||||
offset += blockSize;
|
||||
bytesToDecrypt -= blockSize;
|
||||
}
|
||||
KgmCrypto._free(pQmcBuf);
|
||||
|
||||
result.data = MergeUint8Array(decryptedParts);
|
||||
result.success = true;
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -38,7 +38,7 @@ export async function Decrypt(file: File, raw_filename: string, _: string): Prom
|
||||
let musicBlob = new Blob([audioData], { type: mime });
|
||||
|
||||
const musicMeta = await metaParseBlob(musicBlob);
|
||||
const { title, artist } = GetMetaFromFile(raw_filename, musicMeta.common.title, musicMeta.common.artist);
|
||||
const { title, artist } = GetMetaFromFile(raw_filename, musicMeta.common.title, musicMeta.common.artists == undefined ? musicMeta.common.artist : musicMeta.common.artists.toString());
|
||||
return {
|
||||
album: musicMeta.common.album,
|
||||
picture: GetCoverFromFile(musicMeta),
|
||||
|
||||
@@ -13,7 +13,7 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string)
|
||||
const ext = SniffAudioExt(buffer, raw_ext);
|
||||
if (ext !== raw_ext) file = new Blob([buffer], { type: AudioMimeType[ext] });
|
||||
const tag = await metaParseBlob(file);
|
||||
const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, tag.common.artist);
|
||||
const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, tag.common.artists == undefined ? tag.common.artist : tag.common.artists.toString());
|
||||
|
||||
return {
|
||||
title,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { AudioMimeType, GetArrayBuffer, SniffAudioExt } from '@/decrypt/utils';
|
||||
|
||||
import { DecryptResult } from '@/decrypt/entity';
|
||||
import { QmcDeriveKey } from '@/decrypt/qmc_key';
|
||||
import { DecryptQMCWasm } from '@/decrypt/qmc_wasm';
|
||||
import { DecryptQmcWasm } from '@/decrypt/qmc_wasm';
|
||||
import { extractQQMusicMeta } from '@/utils/qm_meta';
|
||||
|
||||
interface Handler {
|
||||
@@ -24,9 +24,9 @@ export const HandlerMap: { [key: string]: Handler } = {
|
||||
qmcflac: { ext: 'flac', version: 2 },
|
||||
qmcogg: { ext: 'ogg', version: 2 },
|
||||
|
||||
qmc0: { ext: 'mp3', version: 1 },
|
||||
qmc2: { ext: 'ogg', version: 1 },
|
||||
qmc3: { ext: 'mp3', version: 1 },
|
||||
qmc0: { ext: 'mp3', version: 2 },
|
||||
qmc2: { ext: 'ogg', version: 2 },
|
||||
qmc3: { ext: 'mp3', version: 2 },
|
||||
bkcmp3: { ext: 'mp3', version: 1 },
|
||||
bkcflac: { ext: 'flac', version: 1 },
|
||||
tkm: { ext: 'm4a', version: 1 },
|
||||
@@ -49,13 +49,14 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string)
|
||||
if (version === 2 && globalThis.WebAssembly) {
|
||||
console.log('qmc: using wasm decoder');
|
||||
|
||||
const v2Decrypted = await DecryptQMCWasm(fileBuffer);
|
||||
const v2Decrypted = await DecryptQmcWasm(fileBuffer, raw_ext);
|
||||
// 若 v2 检测失败,降级到 v1 再尝试一次
|
||||
if (v2Decrypted.success) {
|
||||
musicDecoded = v2Decrypted.data;
|
||||
musicID = v2Decrypted.songId;
|
||||
console.log('qmc wasm decoder suceeded');
|
||||
} else {
|
||||
console.warn('qmc2-wasm failed with error %s', v2Decrypted.error || '(no error)');
|
||||
console.warn('QmcWasm failed with error %s', v2Decrypted.error || '(no error)');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,7 +152,7 @@ export class QmcDecoder {
|
||||
} else {
|
||||
const sizeView = new DataView(last4Byte.buffer, last4Byte.byteOffset);
|
||||
const keySize = sizeView.getUint32(0, true);
|
||||
if (keySize < 0x300) {
|
||||
if (keySize < 0x400) {
|
||||
this.audioSize = this.size - keySize - 4;
|
||||
const rawKey = this.file.subarray(this.audioSize, this.size - 4);
|
||||
this.setCipher(rawKey);
|
||||
|
||||
@@ -5,12 +5,14 @@ const ZERO_LEN = 7;
|
||||
|
||||
export function QmcDeriveKey(raw: Uint8Array): Uint8Array {
|
||||
const textDec = new TextDecoder();
|
||||
const rawDec = Buffer.from(textDec.decode(raw), 'base64');
|
||||
let rawDec = Buffer.from(textDec.decode(raw), 'base64');
|
||||
let n = rawDec.length;
|
||||
if (n < 16) {
|
||||
throw Error('key length is too short');
|
||||
}
|
||||
|
||||
rawDec = decryptV2Key(rawDec);
|
||||
|
||||
const simpleKey = simpleMakeKey(106, 8);
|
||||
let teaKey = new Uint8Array(16);
|
||||
for (let i = 0; i < 8; i++) {
|
||||
@@ -32,6 +34,30 @@ export function simpleMakeKey(salt: number, length: number): number[] {
|
||||
return keyBuf;
|
||||
}
|
||||
|
||||
const mixKey1: Uint8Array = new Uint8Array([ 0x33, 0x38, 0x36, 0x5A, 0x4A, 0x59, 0x21, 0x40, 0x23, 0x2A, 0x24, 0x25, 0x5E, 0x26, 0x29, 0x28 ])
|
||||
const mixKey2: Uint8Array = new Uint8Array([ 0x2A, 0x2A, 0x23, 0x21, 0x28, 0x23, 0x24, 0x25, 0x26, 0x5E, 0x61, 0x31, 0x63, 0x5A, 0x2C, 0x54 ])
|
||||
|
||||
const v2KeyPrefix: Uint8Array = new Uint8Array([ 0x51, 0x51, 0x4D, 0x75, 0x73, 0x69, 0x63, 0x20, 0x45, 0x6E, 0x63, 0x56, 0x32, 0x2C, 0x4B, 0x65, 0x79, 0x3A ])
|
||||
|
||||
function decryptV2Key(key: Buffer): Buffer
|
||||
{
|
||||
const textEnc = new TextDecoder();
|
||||
if (key.length < 18 || textEnc.decode(key.slice(0, 18)) !== 'QQMusic EncV2,Key:') {
|
||||
return key;
|
||||
}
|
||||
|
||||
let out = decryptTencentTea(key.slice(18), mixKey1);
|
||||
out = decryptTencentTea(out, mixKey2);
|
||||
const textDec = new TextDecoder();
|
||||
const keyDec = Buffer.from(textDec.decode(out), 'base64');
|
||||
let n = keyDec.length;
|
||||
if (n < 16) {
|
||||
throw Error('EncV2 key decode failed');
|
||||
}
|
||||
|
||||
return keyDec;
|
||||
}
|
||||
|
||||
function decryptTencentTea(inBuf: Uint8Array, key: Uint8Array): Uint8Array {
|
||||
if (inBuf.length % 8 != 0) {
|
||||
throw Error('inBuf size not a multiple of the block size');
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import QMCCryptoModule from '@jixun/qmc2-crypto/QMC2-wasm-bundle';
|
||||
import QmcCryptoModule from '@/QmcWasm/QmcWasmBundle';
|
||||
import { MergeUint8Array } from '@/utils/MergeUint8Array';
|
||||
import { QMCCrypto } from '@jixun/qmc2-crypto/QMCCrypto';
|
||||
|
||||
// 检测文件末端使用的缓冲区大小
|
||||
const DETECTION_SIZE = 40;
|
||||
|
||||
// 每次处理 2M 的数据
|
||||
const DECRYPTION_BUF_SIZE = 2 * 1024 * 1024;
|
||||
const DECRYPTION_BUF_SIZE = 2 *1024 * 1024;
|
||||
|
||||
export interface QMC2DecryptionResult {
|
||||
export interface QMCDecryptionResult {
|
||||
success: boolean;
|
||||
data: Uint8Array;
|
||||
songId: string | number;
|
||||
@@ -16,96 +12,62 @@ export interface QMC2DecryptionResult {
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密一个 QMC2 加密的文件。
|
||||
* 解密一个 QMC 加密的文件。
|
||||
*
|
||||
* 如果检测并解密成功,返回解密后的 Uint8Array 数据。
|
||||
* @param {ArrayBuffer} mggBlob 读入的文件 Blob
|
||||
* @param {ArrayBuffer} qmcBlob 读入的文件 Blob
|
||||
*/
|
||||
export async function DecryptQMCWasm(mggBlob: ArrayBuffer): Promise<QMC2DecryptionResult> {
|
||||
const result: QMC2DecryptionResult = { success: false, data: new Uint8Array(), songId: 0, error: '' };
|
||||
export async function DecryptQmcWasm(qmcBlob: ArrayBuffer, ext: string): Promise<QMCDecryptionResult> {
|
||||
const result: QMCDecryptionResult = { success: false, data: new Uint8Array(), songId: 0, error: '' };
|
||||
|
||||
// 初始化模组
|
||||
let QMCCrypto: QMCCrypto;
|
||||
let QmcCrypto: any;
|
||||
|
||||
try {
|
||||
QMCCrypto = await QMCCryptoModule();
|
||||
QmcCrypto = await QmcCryptoModule();
|
||||
} catch (err: any) {
|
||||
result.error = err?.message || 'wasm 加载失败';
|
||||
return result;
|
||||
}
|
||||
|
||||
// 申请内存块,并文件末端数据到 WASM 的内存堆
|
||||
const detectionBuf = new Uint8Array(mggBlob.slice(-DETECTION_SIZE));
|
||||
const pDetectionBuf = QMCCrypto._malloc(detectionBuf.length);
|
||||
QMCCrypto.writeArrayToMemory(detectionBuf, pDetectionBuf);
|
||||
|
||||
// 检测结果内存块
|
||||
const pDetectionResult = QMCCrypto._malloc(QMCCrypto.sizeof_qmc_detection());
|
||||
|
||||
// 进行检测
|
||||
const detectOK = QMCCrypto.detectKeyEndPosition(pDetectionResult, pDetectionBuf, detectionBuf.length);
|
||||
|
||||
// 提取结构体内容:
|
||||
// (pos: i32; len: i32; error: char[??])
|
||||
const position = QMCCrypto.getValue(pDetectionResult, 'i32');
|
||||
const len = QMCCrypto.getValue(pDetectionResult + 4, 'i32');
|
||||
|
||||
result.success = detectOK;
|
||||
result.error = QMCCrypto.UTF8ToString(
|
||||
pDetectionResult + QMCCrypto.offsetof_error_msg(),
|
||||
QMCCrypto.sizeof_error_msg(),
|
||||
);
|
||||
const songId = QMCCrypto.UTF8ToString(pDetectionResult + QMCCrypto.offsetof_song_id(), QMCCrypto.sizeof_song_id());
|
||||
if (!songId) {
|
||||
console.debug('qmc2-wasm: songId not found');
|
||||
} else if (/^\d+$/.test(songId)) {
|
||||
result.songId = songId;
|
||||
} else {
|
||||
console.warn('qmc2-wasm: Invalid songId: %s', songId);
|
||||
}
|
||||
|
||||
// 释放内存
|
||||
QMCCrypto._free(pDetectionBuf);
|
||||
QMCCrypto._free(pDetectionResult);
|
||||
|
||||
if (!detectOK) {
|
||||
if (!QmcCrypto) {
|
||||
result.error = 'wasm 加载失败';
|
||||
return result;
|
||||
}
|
||||
|
||||
// 计算解密后文件的大小。
|
||||
// 之前得到的 position 为相对当前检测数据起点的偏移。
|
||||
const decryptedSize = mggBlob.byteLength - DETECTION_SIZE + position;
|
||||
// 申请内存块,并文件末端数据到 WASM 的内存堆
|
||||
const qmcBuf = new Uint8Array(qmcBlob);
|
||||
const pQmcBuf = QmcCrypto._malloc(DECRYPTION_BUF_SIZE);
|
||||
QmcCrypto.writeArrayToMemory(qmcBuf.slice(-DECRYPTION_BUF_SIZE), pQmcBuf);
|
||||
|
||||
// 提取嵌入到文件的 EKey
|
||||
const ekey = new Uint8Array(mggBlob.slice(decryptedSize, decryptedSize + len));
|
||||
|
||||
// 解码 UTF-8 数据到 string
|
||||
const decoder = new TextDecoder();
|
||||
const ekey_b64 = decoder.decode(ekey);
|
||||
|
||||
// 初始化加密与缓冲区
|
||||
const hCrypto = QMCCrypto.createInstWidthEKey(ekey_b64);
|
||||
const buf = QMCCrypto._malloc(DECRYPTION_BUF_SIZE);
|
||||
// 进行解密初始化
|
||||
ext = '.' + ext;
|
||||
const tailSize = QmcCrypto.preDec(pQmcBuf, DECRYPTION_BUF_SIZE, ext);
|
||||
if (tailSize == -1) {
|
||||
result.error = QmcCrypto.getError();
|
||||
return result;
|
||||
} else {
|
||||
result.songId = QmcCrypto.getSongId();
|
||||
result.songId = result.songId == "0" ? 0 : result.songId;
|
||||
}
|
||||
|
||||
const decryptedParts = [];
|
||||
let offset = 0;
|
||||
let bytesToDecrypt = decryptedSize;
|
||||
let bytesToDecrypt = qmcBuf.length - tailSize;
|
||||
while (bytesToDecrypt > 0) {
|
||||
const blockSize = Math.min(bytesToDecrypt, DECRYPTION_BUF_SIZE);
|
||||
|
||||
// 解密一些片段
|
||||
const blockData = new Uint8Array(mggBlob.slice(offset, offset + blockSize));
|
||||
QMCCrypto.writeArrayToMemory(blockData, buf);
|
||||
QMCCrypto.decryptStream(hCrypto, buf, offset, blockSize);
|
||||
decryptedParts.push(QMCCrypto.HEAPU8.slice(buf, buf + blockSize));
|
||||
const blockData = new Uint8Array(qmcBuf.slice(offset, offset + blockSize));
|
||||
QmcCrypto.writeArrayToMemory(blockData, pQmcBuf);
|
||||
decryptedParts.push(QmcCrypto.HEAPU8.slice(pQmcBuf, pQmcBuf + QmcCrypto.decBlob(pQmcBuf, blockSize, offset)));
|
||||
|
||||
offset += blockSize;
|
||||
bytesToDecrypt -= blockSize;
|
||||
}
|
||||
QMCCrypto._free(buf);
|
||||
hCrypto.delete();
|
||||
QmcCrypto._free(pQmcBuf);
|
||||
|
||||
result.data = MergeUint8Array(decryptedParts);
|
||||
result.success = true;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -8,34 +8,53 @@ import {
|
||||
} from '@/decrypt/utils';
|
||||
|
||||
import { Decrypt as QmcDecrypt, HandlerMap } from '@/decrypt/qmc';
|
||||
import { DecryptQmcWasm } from '@/decrypt/qmc_wasm';
|
||||
|
||||
import { DecryptResult } from '@/decrypt/entity';
|
||||
|
||||
import { parseBlob as metaParseBlob } from 'music-metadata-browser';
|
||||
|
||||
export async function Decrypt(file: Blob, raw_filename: string, _: string): Promise<DecryptResult> {
|
||||
const buffer = new Uint8Array(await GetArrayBuffer(file));
|
||||
let length = buffer.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
buffer[i] ^= 0xf4;
|
||||
if (buffer[i] <= 0x3f) buffer[i] = buffer[i] * 4;
|
||||
else if (buffer[i] <= 0x7f) buffer[i] = (buffer[i] - 0x40) * 4 + 1;
|
||||
else if (buffer[i] <= 0xbf) buffer[i] = (buffer[i] - 0x80) * 4 + 2;
|
||||
else buffer[i] = (buffer[i] - 0xc0) * 4 + 3;
|
||||
export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string): Promise<DecryptResult> {
|
||||
const buffer = await GetArrayBuffer(file);
|
||||
|
||||
let musicDecoded: Uint8Array | undefined;
|
||||
if (globalThis.WebAssembly) {
|
||||
console.log('qmc: using wasm decoder');
|
||||
|
||||
const qmcDecrypted = await DecryptQmcWasm(buffer, raw_ext);
|
||||
// 若 qmc 检测失败,降级到 v1 再尝试一次
|
||||
if (qmcDecrypted.success) {
|
||||
musicDecoded = qmcDecrypted.data;
|
||||
console.log('qmc wasm decoder suceeded');
|
||||
} else {
|
||||
console.warn('QmcWasm failed with error %s', qmcDecrypted.error || '(no error)');
|
||||
}
|
||||
}
|
||||
let ext = SniffAudioExt(buffer, '');
|
||||
|
||||
if (!musicDecoded) {
|
||||
musicDecoded = new Uint8Array(buffer);
|
||||
let length = musicDecoded.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
musicDecoded[i] ^= 0xf4;
|
||||
if (musicDecoded[i] <= 0x3f) musicDecoded[i] = musicDecoded[i] * 4;
|
||||
else if (musicDecoded[i] <= 0x7f) musicDecoded[i] = (musicDecoded[i] - 0x40) * 4 + 1;
|
||||
else if (musicDecoded[i] <= 0xbf) musicDecoded[i] = (musicDecoded[i] - 0x80) * 4 + 2;
|
||||
else musicDecoded[i] = (musicDecoded[i] - 0xc0) * 4 + 3;
|
||||
}
|
||||
}
|
||||
let ext = SniffAudioExt(musicDecoded, '');
|
||||
const newName = SplitFilename(raw_filename);
|
||||
let audioBlob: Blob;
|
||||
if (ext !== '' || newName.ext === 'mp3') {
|
||||
audioBlob = new Blob([buffer], { type: AudioMimeType[ext] });
|
||||
audioBlob = new Blob([musicDecoded], { type: AudioMimeType[ext] });
|
||||
} else if (newName.ext in HandlerMap) {
|
||||
audioBlob = new Blob([buffer], { type: 'application/octet-stream' });
|
||||
audioBlob = new Blob([musicDecoded], { type: 'application/octet-stream' });
|
||||
return QmcDecrypt(audioBlob, newName.name, newName.ext);
|
||||
} else {
|
||||
throw '不支持的QQ音乐缓存格式';
|
||||
}
|
||||
const tag = await metaParseBlob(audioBlob);
|
||||
const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, tag.common.artist);
|
||||
const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, tag.common.artists == undefined ? tag.common.artist : tag.common.artists.toString());
|
||||
|
||||
return {
|
||||
title,
|
||||
|
||||
@@ -17,7 +17,7 @@ export async function Decrypt(
|
||||
if (ext !== raw_ext) file = new Blob([buffer], { type: AudioMimeType[ext] });
|
||||
}
|
||||
const tag = await metaParseBlob(file);
|
||||
const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, tag.common.artist);
|
||||
const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, tag.common.artists == undefined ? tag.common.artist : tag.common.artists.toString());
|
||||
|
||||
return {
|
||||
title,
|
||||
|
||||
@@ -2,6 +2,8 @@ import { IAudioMetadata } from 'music-metadata-browser';
|
||||
import ID3Writer from 'browser-id3-writer';
|
||||
import MetaFlac from 'metaflac-js';
|
||||
|
||||
export const split_regex = /[ ]?[,;/_、][ ]?/;
|
||||
|
||||
export const FLAC_HEADER = [0x66, 0x4c, 0x61, 0x43];
|
||||
export const MP3_HEADER = [0x49, 0x44, 0x33];
|
||||
export const OGG_HEADER = [0x4f, 0x67, 0x67, 0x53];
|
||||
@@ -91,7 +93,7 @@ export function GetMetaFromFile(
|
||||
|
||||
const items = filename.split(separator);
|
||||
if (items.length > 1) {
|
||||
if (!meta.artist) meta.artist = items[0].trim();
|
||||
if (!meta.artist || meta.artist.split(split_regex).length < items[0].trim().split(split_regex).length) meta.artist = items[0].trim();
|
||||
if (!meta.title) meta.title = items[1].trim();
|
||||
} else if (items.length === 1) {
|
||||
if (!meta.title) meta.title = items[0].trim();
|
||||
@@ -119,6 +121,8 @@ export interface IMusicMeta {
|
||||
title: string;
|
||||
artists?: string[];
|
||||
album?: string;
|
||||
albumartist?: string;
|
||||
genre?: string[];
|
||||
picture?: ArrayBuffer;
|
||||
picture_desc?: string;
|
||||
}
|
||||
@@ -169,6 +173,83 @@ export function WriteMetaToFlac(audioData: Buffer, info: IMusicMeta, original: I
|
||||
return writer.save();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
export function SplitFilename(n: string): { name: string; ext: string } {
|
||||
const pos = n.lastIndexOf('.');
|
||||
return {
|
||||
|
||||
@@ -49,7 +49,7 @@ export async function Decrypt(file: File, raw_filename: string, raw_ext: string)
|
||||
const { title, artist } = GetMetaFromFile(
|
||||
raw_filename,
|
||||
musicMeta.common.title,
|
||||
musicMeta.common.artist,
|
||||
musicMeta.common.artists == undefined ? musicMeta.common.artist : musicMeta.common.artists.toString(),
|
||||
raw_filename.indexOf('_') === -1 ? '-' : '_',
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user