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