Merge refactoring (#4)
Removed saveDownloadQueue and tagsLanguage from lib settings Revert embedded cover change Fixed bitrate fallback check Use overwriteFile setting when downloading embedded covers Fixed bitrate fallback not working Fixed some issues to make the lib work Implemented spotify plugin back Better handling of albums upcs Fixed queue item not cancelling correctly Code parity with deemix-js Code cleanup with pylint Even more rework on the library More work on the library (WIP) Total rework of the library (WIP) Some rework done on types Added start queue function Made nextitem work on a thread Removed dz as first parameter Started queuemanager refactoring Removed eventlet Co-authored-by: RemixDev <RemixDev64@gmail.com> Reviewed-on: https://git.freezer.life/RemixDev/deemix-py/pulls/4 Co-Authored-By: RemixDev <remixdev@noreply.localhost> Co-Committed-By: RemixDev <remixdev@noreply.localhost>
This commit is contained in:
156
deemix/decryption.py
Normal file
156
deemix/decryption.py
Normal file
@ -0,0 +1,156 @@
|
||||
from ssl import SSLError
|
||||
from time import sleep
|
||||
import logging
|
||||
|
||||
from requests import get
|
||||
from requests.exceptions import ConnectionError as RequestsConnectionError, ReadTimeout, ChunkedEncodingError
|
||||
from urllib3.exceptions import SSLError as u3SSLError
|
||||
|
||||
from deemix.utils.crypto import _md5, _ecbCrypt, _ecbDecrypt, generateBlowfishKey, decryptChunk
|
||||
|
||||
from deemix.utils import USER_AGENT_HEADER
|
||||
from deemix.types.DownloadObjects import Single
|
||||
|
||||
logger = logging.getLogger('deemix')
|
||||
|
||||
def generateStreamPath(sng_id, md5, media_version, media_format):
|
||||
urlPart = b'\xa4'.join(
|
||||
[md5.encode(), str(media_format).encode(), str(sng_id).encode(), str(media_version).encode()])
|
||||
md5val = _md5(urlPart)
|
||||
step2 = md5val.encode() + b'\xa4' + urlPart + b'\xa4'
|
||||
step2 = step2 + (b'.' * (16 - (len(step2) % 16)))
|
||||
urlPart = _ecbCrypt('jo6aey6haid2Teih', step2)
|
||||
return urlPart.decode("utf-8")
|
||||
|
||||
def reverseStreamPath(urlPart):
|
||||
step2 = _ecbDecrypt('jo6aey6haid2Teih', urlPart)
|
||||
(_, md5, media_format, sng_id, media_version, _) = step2.split(b'\xa4')
|
||||
return (sng_id.decode('utf-8'), md5.decode('utf-8'), media_version.decode('utf-8'), media_format.decode('utf-8'))
|
||||
|
||||
def generateCryptedStreamURL(sng_id, md5, media_version, media_format):
|
||||
urlPart = generateStreamPath(sng_id, md5, media_version, media_format)
|
||||
return "https://e-cdns-proxy-" + md5[0] + ".dzcdn.net/mobile/1/" + urlPart
|
||||
|
||||
def generateStreamURL(sng_id, md5, media_version, media_format):
|
||||
urlPart = generateStreamPath(sng_id, md5, media_version, media_format)
|
||||
return "https://e-cdns-proxy-" + md5[0] + ".dzcdn.net/api/1/" + urlPart
|
||||
|
||||
def reverseStreamURL(url):
|
||||
urlPart = url[url.find("/1/")+3:]
|
||||
return reverseStreamPath(urlPart)
|
||||
|
||||
def streamTrack(outputStream, track, start=0, downloadObject=None, listener=None):
|
||||
if downloadObject.isCanceled: raise DownloadCanceled
|
||||
headers= {'User-Agent': USER_AGENT_HEADER}
|
||||
chunkLength = start
|
||||
|
||||
itemName = f"[{track.mainArtist.name} - {track.title}]"
|
||||
|
||||
try:
|
||||
with get(track.downloadUrl, headers=headers, stream=True, timeout=10) as request:
|
||||
request.raise_for_status()
|
||||
|
||||
complete = int(request.headers["Content-Length"])
|
||||
if complete == 0: raise DownloadEmpty
|
||||
if start != 0:
|
||||
responseRange = request.headers["Content-Range"]
|
||||
if listener:
|
||||
listener.send('downloadInfo', {
|
||||
'uuid': downloadObject.uuid,
|
||||
'itemName': itemName,
|
||||
'state': "downloading",
|
||||
'alreadyStarted': True,
|
||||
'value': responseRange
|
||||
})
|
||||
else:
|
||||
if listener:
|
||||
listener.send('downloadInfo', {
|
||||
'uuid': downloadObject.uuid,
|
||||
'itemName': itemName,
|
||||
'state': "downloading",
|
||||
'alreadyStarted': False,
|
||||
'value': complete
|
||||
})
|
||||
|
||||
for chunk in request.iter_content(2048 * 3):
|
||||
outputStream.write(chunk)
|
||||
chunkLength += len(chunk)
|
||||
|
||||
if downloadObject:
|
||||
if isinstance(downloadObject, Single):
|
||||
chunkProgres = (chunkLength / (complete + start)) * 100
|
||||
downloadObject.progressNext = chunkProgres
|
||||
else:
|
||||
chunkProgres = (len(chunk) / (complete + start)) / downloadObject.size * 100
|
||||
downloadObject.progressNext += chunkProgres
|
||||
downloadObject.updateProgress(listener)
|
||||
|
||||
except (SSLError, u3SSLError):
|
||||
logger.info('%s retrying from byte %s', itemName, chunkLength)
|
||||
streamTrack(outputStream, track, chunkLength, downloadObject, listener)
|
||||
except (RequestsConnectionError, ReadTimeout, ChunkedEncodingError):
|
||||
sleep(2)
|
||||
streamTrack(outputStream, track, start, downloadObject, listener)
|
||||
|
||||
def streamCryptedTrack(outputStream, track, start=0, downloadObject=None, listener=None):
|
||||
if downloadObject.isCanceled: raise DownloadCanceled
|
||||
headers= {'User-Agent': USER_AGENT_HEADER}
|
||||
chunkLength = start
|
||||
|
||||
itemName = f"[{track.mainArtist.name} - {track.title}]"
|
||||
|
||||
try:
|
||||
with get(track.downloadUrl, headers=headers, stream=True, timeout=10) as request:
|
||||
request.raise_for_status()
|
||||
blowfish_key = str.encode(generateBlowfishKey(str(track.id)))
|
||||
|
||||
complete = int(request.headers["Content-Length"])
|
||||
if complete == 0: raise DownloadEmpty
|
||||
if start != 0:
|
||||
responseRange = request.headers["Content-Range"]
|
||||
if listener:
|
||||
listener.send('downloadInfo', {
|
||||
'uuid': downloadObject.uuid,
|
||||
'itemName': itemName,
|
||||
'state': "downloading",
|
||||
'alreadyStarted': True,
|
||||
'value': responseRange
|
||||
})
|
||||
else:
|
||||
if listener:
|
||||
listener.send('downloadInfo', {
|
||||
'uuid': downloadObject.uuid,
|
||||
'itemName': itemName,
|
||||
'state': "downloading",
|
||||
'alreadyStarted': False,
|
||||
'value': complete
|
||||
})
|
||||
|
||||
for chunk in request.iter_content(2048 * 3):
|
||||
if len(chunk) >= 2048:
|
||||
chunk = decryptChunk(blowfish_key, chunk[0:2048]) + chunk[2048:]
|
||||
|
||||
outputStream.write(chunk)
|
||||
chunkLength += len(chunk)
|
||||
|
||||
if downloadObject:
|
||||
if isinstance(downloadObject, Single):
|
||||
chunkProgres = (chunkLength / (complete + start)) * 100
|
||||
downloadObject.progressNext = chunkProgres
|
||||
else:
|
||||
chunkProgres = (len(chunk) / (complete + start)) / downloadObject.size * 100
|
||||
downloadObject.progressNext += chunkProgres
|
||||
downloadObject.updateProgress(listener)
|
||||
|
||||
except (SSLError, u3SSLError):
|
||||
logger.info('%s retrying from byte %s', itemName, chunkLength)
|
||||
streamCryptedTrack(outputStream, track, chunkLength, downloadObject, listener)
|
||||
except (RequestsConnectionError, ReadTimeout, ChunkedEncodingError):
|
||||
sleep(2)
|
||||
streamCryptedTrack(outputStream, track, start, downloadObject, listener)
|
||||
|
||||
class DownloadCanceled(Exception):
|
||||
pass
|
||||
|
||||
class DownloadEmpty(Exception):
|
||||
pass
|
Reference in New Issue
Block a user