MengYX
5 years ago
9 changed files with 323 additions and 346 deletions
@ -0,0 +1,44 @@ |
|||||
|
const NcmDecrypt = require("./ncm"); |
||||
|
const QmcDecrypt = require("./qmc"); |
||||
|
const RawDecrypt = require("./raw"); |
||||
|
const MFlacDecrypt = require("./mflac"); |
||||
|
|
||||
|
export {CommonDecrypt} |
||||
|
|
||||
|
async function CommonDecrypt(file) { |
||||
|
let raw_ext = file.name.substring(file.name.lastIndexOf(".") + 1, file.name.length).toLowerCase(); |
||||
|
let raw_filename = file.name.substring(0, file.name.lastIndexOf(".")); |
||||
|
let rt_data; |
||||
|
switch (raw_ext) { |
||||
|
case "ncm":// Netease Mp3/Flac
|
||||
|
rt_data = await NcmDecrypt.Decrypt(file.raw); |
||||
|
break; |
||||
|
case "mp3":// Raw Mp3
|
||||
|
case "flac"://Raw Flac
|
||||
|
case "m4a":// todo: Raw M4A
|
||||
|
case "tm0":// QQ Music IOS Mp3
|
||||
|
case "tm3":// QQ Music IOS Mp3
|
||||
|
rt_data = await RawDecrypt.Decrypt(file.raw, raw_filename, raw_ext); |
||||
|
break; |
||||
|
case "qmc3"://QQ Music Android Mp3
|
||||
|
case "qmc0"://QQ Music Android Mp3
|
||||
|
case "qmcflac"://QQ Music Android Flac
|
||||
|
case "qmcogg"://QQ Music Android Ogg
|
||||
|
rt_data = await QmcDecrypt.Decrypt(file.raw, raw_filename, raw_ext); |
||||
|
break; |
||||
|
case "mflac"://QQ Music Desktop Flac
|
||||
|
rt_data = await MFlacDecrypt.Decrypt(file.raw, raw_filename, raw_ext); |
||||
|
break; |
||||
|
case "tm2":// todo: QQ Music IOS M4A
|
||||
|
case "tm6":// todo: QQ Music IOS M4A
|
||||
|
debugger; |
||||
|
break; |
||||
|
default: |
||||
|
rt_data = {status: false, message: "不支持此文件格式",} |
||||
|
} |
||||
|
if (rt_data.status) { |
||||
|
rt_data.rawExt = raw_ext; |
||||
|
rt_data.rawFilename = raw_filename; |
||||
|
} |
||||
|
return rt_data; |
||||
|
} |
@ -0,0 +1,171 @@ |
|||||
|
const CryptoJS = require("crypto-js"); |
||||
|
const ID3Writer = require("browser-id3-writer"); |
||||
|
const util = require("./util"); |
||||
|
const CORE_KEY = CryptoJS.enc.Hex.parse("687a4852416d736f356b496e62617857"); |
||||
|
const META_KEY = CryptoJS.enc.Hex.parse("2331346C6A6B5F215C5D2630553C2728"); |
||||
|
|
||||
|
|
||||
|
export {Decrypt}; |
||||
|
|
||||
|
async function Decrypt(file) { |
||||
|
|
||||
|
const fileBuffer = await util.GetArrayBuffer(file); |
||||
|
const dataView = new DataView(fileBuffer); |
||||
|
|
||||
|
if (dataView.getUint32(0, true) !== 0x4e455443 || |
||||
|
dataView.getUint32(4, true) !== 0x4d414446) |
||||
|
return {status: false, message: "此ncm文件已损坏"}; |
||||
|
|
||||
|
|
||||
|
const keyDataObj = getKeyData(dataView, fileBuffer, 10); |
||||
|
const keyBox = getKeyBox(keyDataObj.data); |
||||
|
|
||||
|
const musicMetaObj = getMetaData(dataView, fileBuffer, keyDataObj.offset); |
||||
|
const musicMeta = musicMetaObj.data; |
||||
|
|
||||
|
let audioOffset = musicMetaObj.offset + dataView.getUint32(musicMetaObj.offset + 5, true) + 13; |
||||
|
let audioData = new Uint8Array(fileBuffer, audioOffset); |
||||
|
|
||||
|
for (let cur = 0; cur < audioData.length; ++cur) { |
||||
|
audioData[cur] ^= keyBox[cur & 0xff]; |
||||
|
} |
||||
|
|
||||
|
if (musicMeta.format === undefined) { |
||||
|
const [f, L, a, C] = audioData; |
||||
|
if (f === 0x66 && L === 0x4c && a === 0x61 && C === 0x43) { |
||||
|
musicMeta.format = "flac"; |
||||
|
} else { |
||||
|
musicMeta.format = "mp3"; |
||||
|
} |
||||
|
} |
||||
|
const mime = util.AudioMimeType[musicMeta.format]; |
||||
|
|
||||
|
const artists = []; |
||||
|
musicMeta.artist.forEach(arr => { |
||||
|
artists.push(arr[0]); |
||||
|
}); |
||||
|
if (musicMeta.format === "mp3") { |
||||
|
audioData = await writeID3(audioData, artists, musicMeta.musicName, musicMeta.album, musicMeta.albumPic) |
||||
|
} |
||||
|
|
||||
|
const musicData = new Blob([audioData], {type: mime}); |
||||
|
const musicUrl = URL.createObjectURL(musicData); |
||||
|
const filename = artists.join(" & ") + " - " + musicMeta.musicName + "." + musicMeta.format; |
||||
|
return { |
||||
|
status: true, |
||||
|
filename: filename, |
||||
|
title: musicMeta.musicName, |
||||
|
artist: artists.join(" & "), |
||||
|
album: musicMeta.album, |
||||
|
picture: musicMeta.albumPic, |
||||
|
file: musicUrl, |
||||
|
mime: mime |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
async function writeID3(audioData, artistList, title, album, picture) { |
||||
|
const writer = new ID3Writer(audioData); |
||||
|
writer.setFrame("TPE1", artistList) |
||||
|
.setFrame("TIT2", title) |
||||
|
.setFrame("TALB", album); |
||||
|
if (picture !== "") { |
||||
|
try { |
||||
|
const img = await (await fetch(picture)).arrayBuffer(); |
||||
|
writer.setFrame('APIC', { |
||||
|
type: 3, |
||||
|
data: img, |
||||
|
description: 'Cover' |
||||
|
}) |
||||
|
} catch (e) { |
||||
|
console.log("Fail to write cover image!"); |
||||
|
} |
||||
|
} |
||||
|
writer.addTag(); |
||||
|
return writer.arrayBuffer; |
||||
|
} |
||||
|
|
||||
|
function getKeyData(dataView, fileBuffer, offset) { |
||||
|
const keyLen = dataView.getUint32(offset, true); |
||||
|
offset += 4; |
||||
|
const cipherText = new Uint8Array(fileBuffer, offset, keyLen).map( |
||||
|
uint8 => uint8 ^ 0x64 |
||||
|
); |
||||
|
offset += keyLen; |
||||
|
|
||||
|
const plainText = CryptoJS.AES.decrypt( |
||||
|
{ciphertext: CryptoJS.lib.WordArray.create(cipherText)}, |
||||
|
CORE_KEY, |
||||
|
{ |
||||
|
mode: CryptoJS.mode.ECB, |
||||
|
padding: CryptoJS.pad.Pkcs7 |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
const result = new Uint8Array(plainText.sigBytes); |
||||
|
|
||||
|
const words = plainText.words; |
||||
|
const sigBytes = plainText.sigBytes; |
||||
|
for (let i = 0; i < sigBytes; i++) { |
||||
|
result[i] = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; |
||||
|
} |
||||
|
|
||||
|
return {offset: offset, data: result.slice(17)}; |
||||
|
} |
||||
|
|
||||
|
function getKeyBox(keyData) { |
||||
|
const box = new Uint8Array(Array(256).keys()); |
||||
|
|
||||
|
const keyDataLen = keyData.length; |
||||
|
|
||||
|
let j = 0; |
||||
|
|
||||
|
for (let i = 0; i < 256; i++) { |
||||
|
j = (box[i] + j + keyData[i % keyDataLen]) & 0xff; |
||||
|
[box[i], box[j]] = [box[j], box[i]]; |
||||
|
} |
||||
|
|
||||
|
return box.map((_, i, arr) => { |
||||
|
i = (i + 1) & 0xff; |
||||
|
const si = arr[i]; |
||||
|
const sj = arr[(i + si) & 0xff]; |
||||
|
return arr[(si + sj) & 0xff]; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @typedef {Object} MusicMetaType |
||||
|
* @property {Number} musicId |
||||
|
* @property {String} musicName |
||||
|
* @property {[[String, Number]]} artist |
||||
|
* @property {String} album |
||||
|
* @property {"flac"|"mp3"} format |
||||
|
* @property {String} albumPic |
||||
|
*/ |
||||
|
|
||||
|
function getMetaData(dataView, fileBuffer, offset) { |
||||
|
const metaDataLen = dataView.getUint32(offset, true); |
||||
|
offset += 4; |
||||
|
if (metaDataLen === 0) { |
||||
|
return {}; |
||||
|
} |
||||
|
|
||||
|
const cipherText = new Uint8Array(fileBuffer, offset, metaDataLen).map( |
||||
|
data => data ^ 0x63 |
||||
|
); |
||||
|
offset += metaDataLen; |
||||
|
|
||||
|
const plainText = CryptoJS.AES.decrypt({ |
||||
|
ciphertext: CryptoJS.enc.Base64.parse( |
||||
|
CryptoJS.lib.WordArray.create(cipherText.slice(22)).toString(CryptoJS.enc.Utf8) |
||||
|
) |
||||
|
}, |
||||
|
META_KEY, |
||||
|
{mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7} |
||||
|
); |
||||
|
|
||||
|
const result = JSON.parse(plainText.toString(CryptoJS.enc.Utf8).slice(6)); |
||||
|
result.albumPic = result.albumPic.replace("http:", "https:"); |
||||
|
return {data: result, offset: offset}; |
||||
|
} |
||||
|
|
||||
|
|
@ -0,0 +1,25 @@ |
|||||
|
const musicMetadata = require("music-metadata-browser"); |
||||
|
const util = require("./util"); |
||||
|
export {Decrypt} |
||||
|
|
||||
|
|
||||
|
async function Decrypt(file, raw_filename, raw_ext) { |
||||
|
let tag = await musicMetadata.parseBlob(file); |
||||
|
|
||||
|
let fileUrl = URL.createObjectURL(file); |
||||
|
|
||||
|
const picUrl = util.GetCoverURL(tag); |
||||
|
const mime = util.AudioMimeType[raw_ext]; |
||||
|
const info = util.GetFileInfo(tag.common.artist, tag.common.title, raw_filename, raw_ext); |
||||
|
|
||||
|
return { |
||||
|
status: true, |
||||
|
filename: info.filename, |
||||
|
title: info.title, |
||||
|
artist: info.artist, |
||||
|
album: tag.common.album, |
||||
|
picture: picUrl, |
||||
|
file: fileUrl, |
||||
|
mime: mime |
||||
|
} |
||||
|
} |
@ -0,0 +1,51 @@ |
|||||
|
export {GetArrayBuffer, GetFileInfo, GetCoverURL, AudioMimeType} |
||||
|
|
||||
|
// Also a new draft API: blob.arrayBuffer()
|
||||
|
async function GetArrayBuffer(blobObject) { |
||||
|
return await new Promise(resolve => { |
||||
|
const reader = new FileReader(); |
||||
|
reader.onload = (e) => { |
||||
|
resolve(e.target.result); |
||||
|
}; |
||||
|
reader.readAsArrayBuffer(blobObject); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
const AudioMimeType = { |
||||
|
mp3: "audio/mpeg", |
||||
|
flac: "audio/flac", |
||||
|
m4a: "audio/mp4", |
||||
|
ogg: "audio/ogg" |
||||
|
}; |
||||
|
|
||||
|
function GetFileInfo(artist, title, filenameNoExt, ext) { |
||||
|
let newArtist = "", newTitle = ""; |
||||
|
let filenameArray = filenameNoExt.split("-"); |
||||
|
if (filenameArray.length > 1) { |
||||
|
newArtist = filenameArray[0].trim(); |
||||
|
newTitle = filenameArray[1].trim(); |
||||
|
} else if (filenameArray.length === 1) { |
||||
|
newTitle = filenameArray[0].trim(); |
||||
|
} |
||||
|
|
||||
|
if (typeof artist == "string" && artist !== "") { |
||||
|
newArtist = artist; |
||||
|
} |
||||
|
if (typeof title == "string" && title !== "") { |
||||
|
newTitle = title; |
||||
|
} |
||||
|
let newFilename = newArtist + " - " + newTitle + "." + ext; |
||||
|
return {artist: newArtist, title: newTitle, filename: newFilename}; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @return {string} |
||||
|
*/ |
||||
|
function GetCoverURL(metadata) { |
||||
|
let pic_url = ""; |
||||
|
if (metadata.common.picture !== undefined && metadata.common.picture.length > 0) { |
||||
|
let pic = new Blob([metadata.common.picture[0].data], {type: metadata.common.picture[0].format}); |
||||
|
pic_url = URL.createObjectURL(pic); |
||||
|
} |
||||
|
return pic_url; |
||||
|
} |
@ -1,183 +0,0 @@ |
|||||
const CryptoJS = require("crypto-js"); |
|
||||
const ID3Writer = require("browser-id3-writer"); |
|
||||
const CORE_KEY = CryptoJS.enc.Hex.parse("687a4852416d736f356b496e62617857"); |
|
||||
const META_KEY = CryptoJS.enc.Hex.parse("2331346C6A6B5F215C5D2630553C2728"); |
|
||||
|
|
||||
const audio_mime_type = { |
|
||||
mp3: "audio/mpeg", |
|
||||
flac: "audio/flac" |
|
||||
}; |
|
||||
|
|
||||
export {Decrypt}; |
|
||||
|
|
||||
async function Decrypt(file) { |
|
||||
|
|
||||
const fileBuffer = await new Promise(reslove => { |
|
||||
const reader = new FileReader(); |
|
||||
reader.onload = (e) => { |
|
||||
reslove(e.target.result); |
|
||||
}; |
|
||||
reader.readAsArrayBuffer(file); |
|
||||
}); |
|
||||
|
|
||||
const dataView = new DataView(fileBuffer); |
|
||||
|
|
||||
if (dataView.getUint32(0, true) !== 0x4e455443 || |
|
||||
dataView.getUint32(4, true) !== 0x4d414446 |
|
||||
) return { |
|
||||
status: false, |
|
||||
message: "此ncm文件已损坏", |
|
||||
}; |
|
||||
|
|
||||
let offset = 10; |
|
||||
|
|
||||
const keyData = (() => { |
|
||||
const keyLen = dataView.getUint32(offset, true); |
|
||||
offset += 4; |
|
||||
const cipherText = new Uint8Array(fileBuffer, offset, keyLen).map( |
|
||||
uint8 => uint8 ^ 0x64 |
|
||||
); |
|
||||
offset += keyLen; |
|
||||
|
|
||||
const plainText = CryptoJS.AES.decrypt( |
|
||||
{ciphertext: CryptoJS.lib.WordArray.create(cipherText)}, |
|
||||
CORE_KEY, |
|
||||
{ |
|
||||
mode: CryptoJS.mode.ECB, |
|
||||
padding: CryptoJS.pad.Pkcs7 |
|
||||
} |
|
||||
); |
|
||||
|
|
||||
const result = new Uint8Array(plainText.sigBytes); |
|
||||
|
|
||||
const words = plainText.words; |
|
||||
const sigBytes = plainText.sigBytes; |
|
||||
for (let i = 0; i < sigBytes; i++) { |
|
||||
result[i] = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; |
|
||||
} |
|
||||
|
|
||||
|
|
||||
return result.slice(17); |
|
||||
})(); |
|
||||
|
|
||||
const keyBox = (() => { |
|
||||
const box = new Uint8Array(Array(256).keys()); |
|
||||
|
|
||||
const keyDataLen = keyData.length; |
|
||||
|
|
||||
let j = 0; |
|
||||
|
|
||||
for (let i = 0; i < 256; i++) { |
|
||||
j = (box[i] + j + keyData[i % keyDataLen]) & 0xff; |
|
||||
[box[i], box[j]] = [box[j], box[i]]; |
|
||||
} |
|
||||
|
|
||||
return box.map((_, i, arr) => { |
|
||||
i = (i + 1) & 0xff; |
|
||||
const si = arr[i]; |
|
||||
const sj = arr[(i + si) & 0xff]; |
|
||||
return arr[(si + sj) & 0xff]; |
|
||||
}); |
|
||||
})(); |
|
||||
|
|
||||
/** |
|
||||
* @typedef {Object} MusicMetaType |
|
||||
* @property {Number} musicId |
|
||||
* @property {String} musicName |
|
||||
* @property {[[String, Number]]} artist |
|
||||
* @property {String} album |
|
||||
* @property {"flac"|"mp3"} format |
|
||||
* @property {String} albumPic |
|
||||
*/ |
|
||||
|
|
||||
/** @type {MusicMetaType|undefined} */ |
|
||||
const musicMeta = (() => { |
|
||||
const metaDataLen = dataView.getUint32(offset, true); |
|
||||
offset += 4; |
|
||||
if (metaDataLen === 0) { |
|
||||
return {}; |
|
||||
} |
|
||||
|
|
||||
const cipherText = new Uint8Array(fileBuffer, offset, metaDataLen).map( |
|
||||
data => data ^ 0x63 |
|
||||
); |
|
||||
offset += metaDataLen; |
|
||||
|
|
||||
const plainText = CryptoJS.AES.decrypt( |
|
||||
{ |
|
||||
ciphertext: CryptoJS.enc.Base64.parse( |
|
||||
CryptoJS.lib.WordArray.create(cipherText.slice(22)).toString(CryptoJS.enc.Utf8) |
|
||||
) |
|
||||
}, |
|
||||
META_KEY, |
|
||||
{mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7} |
|
||||
); |
|
||||
|
|
||||
const result = JSON.parse(plainText.toString(CryptoJS.enc.Utf8).slice(6)); |
|
||||
result.albumPic = result.albumPic.replace("http:", "https:"); |
|
||||
return result; |
|
||||
})(); |
|
||||
|
|
||||
offset += dataView.getUint32(offset + 5, true) + 13; |
|
||||
|
|
||||
let audioData = new Uint8Array(fileBuffer, offset); |
|
||||
let audioDataLen = audioData.length; |
|
||||
|
|
||||
|
|
||||
for (let cur = 0; cur < audioDataLen; ++cur) { |
|
||||
audioData[cur] ^= keyBox[cur & 0xff]; |
|
||||
} |
|
||||
|
|
||||
if (musicMeta.format === undefined) { |
|
||||
const [f, L, a, C] = audioData; |
|
||||
if (f === 0x66 && L === 0x4c && a === 0x61 && C === 0x43) { |
|
||||
musicMeta.format = "flac"; |
|
||||
} else { |
|
||||
musicMeta.format = "mp3"; |
|
||||
} |
|
||||
} |
|
||||
const mime = audio_mime_type[musicMeta.format]; |
|
||||
|
|
||||
const artists = []; |
|
||||
musicMeta.artist.forEach(arr => { |
|
||||
artists.push(arr[0]); |
|
||||
}); |
|
||||
|
|
||||
if (musicMeta.format === "mp3") { |
|
||||
const writer = new ID3Writer(audioData); |
|
||||
writer.setFrame("TPE1", artists) |
|
||||
.setFrame("TIT2", musicMeta.musicName) |
|
||||
.setFrame("TALB", musicMeta.album); |
|
||||
if (musicMeta.albumPic !== "") { |
|
||||
try { |
|
||||
const img = await (await fetch(musicMeta.albumPic)).arrayBuffer(); |
|
||||
writer.setFrame('APIC', { |
|
||||
type: 3, |
|
||||
data: img, |
|
||||
description: 'Cover' |
|
||||
}) |
|
||||
} catch (e) { |
|
||||
console.log("Fail to write cover image!"); |
|
||||
} |
|
||||
} |
|
||||
writer.addTag(); |
|
||||
audioData = writer.arrayBuffer; |
|
||||
} |
|
||||
|
|
||||
const musicData = new Blob([audioData], { |
|
||||
type: mime |
|
||||
}); |
|
||||
const musicUrl = URL.createObjectURL(musicData); |
|
||||
const filename = artists.join(" & ") + " - " + musicMeta.musicName + "." + musicMeta.format; |
|
||||
return { |
|
||||
status: true, |
|
||||
filename: filename, |
|
||||
title: musicMeta.musicName, |
|
||||
artist: artists.join(" & "), |
|
||||
album: musicMeta.album, |
|
||||
picture: musicMeta.albumPic, |
|
||||
file: musicUrl, |
|
||||
mime: mime |
|
||||
}; |
|
||||
} |
|
||||
|
|
@ -1,44 +0,0 @@ |
|||||
const musicMetadata = require("music-metadata-browser"); |
|
||||
export {Decrypt} |
|
||||
|
|
||||
const audio_mime_type = { |
|
||||
mp3: "audio/mpeg", |
|
||||
flac: "audio/flac" |
|
||||
}; |
|
||||
|
|
||||
async function Decrypt(file) { |
|
||||
let tag = await musicMetadata.parseBlob(file); |
|
||||
let pic_url = ""; |
|
||||
if (tag.common.picture !== undefined && tag.common.picture.length > 0) { |
|
||||
let pic = new Blob([tag.common.picture[0].data], {type: tag.common.picture[0].format}); |
|
||||
pic_url = URL.createObjectURL(pic); |
|
||||
} |
|
||||
|
|
||||
let file_url = URL.createObjectURL(file); |
|
||||
|
|
||||
let filename_no_ext = file.name.substring(0, file.name.lastIndexOf(".")); |
|
||||
let filename_array = filename_no_ext.split("-"); |
|
||||
let filename_ext = file.name.substring(file.name.lastIndexOf(".") + 1, file.name.length).toLowerCase(); |
|
||||
const mime = audio_mime_type[filename_ext]; |
|
||||
let title = tag.common.title; |
|
||||
let artist = tag.common.artist; |
|
||||
|
|
||||
if (filename_array.length > 1) { |
|
||||
if (artist === undefined) artist = filename_array[0].trim(); |
|
||||
if (title === undefined) title = filename_array[1].trim(); |
|
||||
} else if (filename_array.length === 1) { |
|
||||
if (title === undefined) title = filename_array[0].trim(); |
|
||||
} |
|
||||
|
|
||||
const filename = artist + " - " + title + "." + filename_ext; |
|
||||
return { |
|
||||
status:true, |
|
||||
filename: filename, |
|
||||
title: title, |
|
||||
artist: artist, |
|
||||
album: tag.common.album, |
|
||||
picture: pic_url, |
|
||||
file: file_url, |
|
||||
mime: mime |
|
||||
} |
|
||||
} |
|
Loading…
Reference in new issue