const got = require('got') const fs = require('fs') const {_md5, _ecbCrypt, _ecbDecrypt, generateBlowfishKey, decryptChunk} = require('./utils/crypto.js') const { DownloadCanceled, DownloadEmpty} = require('./errors.js') const { USER_AGENT_HEADER, pipeline } = require('./utils/index.js') function generateStreamPath(sngID, md5, mediaVersion, format){ let urlPart = md5+"¤"+format+"¤"+sngID+"¤"+mediaVersion let md5val = _md5(urlPart) let step2 = md5val+"¤"+urlPart+"¤" step2 += '.'.repeat(16 - (step2.length % 16)) urlPart = _ecbCrypt('jo6aey6haid2Teih', step2) return urlPart } function reverseStreamPath(urlPart){ let step2 = _ecbDecrypt('jo6aey6haid2Teih', urlPart) let [, md5, format, sngID, mediaVersion] = step2.split("¤") return [sngID, md5, mediaVersion, format] } function generateCryptedStreamURL(sngID, md5, mediaVersion, format){ let urlPart = generateStreamPath(sngID, md5, mediaVersion, format) return "https://e-cdns-proxy-" + md5[0] + ".dzcdn.net/mobile/1/" + urlPart } function generateStreamURL(sngID, md5, mediaVersion, format){ let urlPart = generateStreamPath(sngID, md5, mediaVersion, format) return "https://cdns-proxy-" + md5[0] + ".dzcdn.net/api/1/" + urlPart } function reverseStreamURL(url){ let urlPart = url.slice(url.find("/1/")+3) return reverseStreamPath(urlPart) } async function streamTrack(writepath, track, downloadObject, listener){ if (downloadObject && downloadObject.isCanceled) throw new DownloadCanceled let headers = {'User-Agent': USER_AGENT_HEADER} let chunkLength = 0 let complete = 0 let isCryptedStream = track.downloadURL.includes("/mobile/") || track.downloadURL.includes("/media/") let blowfishKey let outputStream = fs.createWriteStream(writepath) let itemData = { id: track.id, title: track.title, artist: track.mainArtist.name } let error = '' if (isCryptedStream) blowfishKey = generateBlowfishKey(String(track.id)) async function* decrypter(source){ let modifiedStream = Buffer.alloc(0) for await (let chunk of source){ if (!isCryptedStream){ yield chunk } else { modifiedStream = Buffer.concat([modifiedStream, chunk]) while (modifiedStream.length >= 2048 * 3){ let decryptedChunks = Buffer.alloc(0) let decryptingChunks = modifiedStream.slice(0, 2048 * 3) modifiedStream = modifiedStream.slice(2048 * 3) if (decryptingChunks.length >= 2048){ decryptedChunks = decryptChunk(decryptingChunks.slice(0, 2048), blowfishKey) decryptedChunks = Buffer.concat([decryptedChunks, decryptingChunks.slice(2048)]) } yield decryptedChunks } } } if (isCryptedStream){ let decryptedChunks = Buffer.alloc(0) if (modifiedStream.length >= 2048){ decryptedChunks = decryptChunk(modifiedStream.slice(0, 2048), blowfishKey) decryptedChunks = Buffer.concat([decryptedChunks, modifiedStream.slice(2048)]) yield decryptedChunks }else{ yield modifiedStream } } } async function* depadder(source){ let isStart = true for await (let chunk of source){ if (isStart && chunk[0] == 0 && chunk.slice(4, 8).toString() !== "ftyp"){ let i for (i = 0; i < chunk.length; i++){ let byte = chunk[i] if (byte !== 0) break } chunk = chunk.slice(i) } isStart = false yield chunk } } let request = got.stream(track.downloadURL, { headers: headers, timeout: 7000, https: {rejectUnauthorized: false} }).on('response', (response)=>{ complete = parseInt(response.headers["content-length"]) if (complete == 0) { error = "DownloadEmpty" request.destroy() } if (listener) listener.send('downloadInfo', { uuid: downloadObject.uuid, data: itemData, state: "downloading", alreadyStarted: false, value: complete }) }).on('data', function(chunk){ if (downloadObject.isCanceled) { error = "DownloadCanceled" request.destroy() } chunkLength += chunk.length if (downloadObject){ downloadObject.progressNext += ((chunk.length / complete) / downloadObject.size) * 100 downloadObject.updateProgress(listener) } }) try { await pipeline(request, decrypter, depadder, outputStream) } catch (e){ outputStream.close() if (e instanceof got.ReadError || e instanceof got.TimeoutError || ["ESOCKETTIMEDOUT", "ERR_STREAM_PREMATURE_CLOSE", "ETIMEDOUT"].includes(e.code)){ if (downloadObject && chunkLength != 0){ downloadObject.progressNext -= ((chunkLength / complete) / downloadObject.size) * 100 downloadObject.updateProgress(listener) } return await streamTrack(writepath, track, downloadObject, listener) } else if (request.destroyed) { switch (error) { case 'DownloadEmpty': throw new DownloadEmpty case 'DownloadCanceled': throw new DownloadCanceled default: throw e } } else { console.trace(e) throw e } } outputStream.close() } module.exports = { generateStreamPath, generateStreamURL, generateCryptedStreamURL, reverseStreamPath, reverseStreamURL, streamTrack }