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