mirror of
https://gitlab.com/RemixDev/deemix-js.git
synced 2024-12-28 18:36:26 +00:00
185 lines
5.7 KiB
JavaScript
185 lines
5.7 KiB
JavaScript
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 timeout = null
|
|
|
|
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,
|
|
https: {rejectUnauthorized: false}
|
|
}).on('response', (response)=>{
|
|
clearTimeout(timeout)
|
|
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)
|
|
}
|
|
clearTimeout(timeout)
|
|
timeout = setTimeout(()=>{
|
|
error = "DownloadTimeout"
|
|
request.destroy()
|
|
}, 5000)
|
|
})
|
|
|
|
timeout = setTimeout(()=>{
|
|
error = "DownloadTimeout"
|
|
request.destroy()
|
|
}, 5000)
|
|
|
|
try {
|
|
await pipeline(request, decrypter, depadder, outputStream)
|
|
} catch (e){
|
|
if (fs.existsSync(writepath)) fs.unlinkSync(writepath)
|
|
if (
|
|
e instanceof got.ReadError ||
|
|
e instanceof got.TimeoutError ||
|
|
["ESOCKETTIMEDOUT", "ERR_STREAM_PREMATURE_CLOSE", "ETIMEDOUT", "ECONNRESET"].includes(e.code) ||
|
|
request.destroyed && error == "DownloadTimeout"
|
|
){
|
|
if (downloadObject && chunkLength != 0){
|
|
downloadObject.progressNext -= ((chunkLength / complete) / downloadObject.size) * 100
|
|
downloadObject.updateProgress(listener)
|
|
}
|
|
if (listener) listener.send('downloadInfo', {
|
|
uuid: downloadObject.uuid,
|
|
data: itemData,
|
|
state: "downloadTimeout"
|
|
})
|
|
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
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
generateStreamPath,
|
|
generateStreamURL,
|
|
generateCryptedStreamURL,
|
|
reverseStreamPath,
|
|
reverseStreamURL,
|
|
streamTrack
|
|
}
|