deemix-py/deemix/decryption.py
RemixDev f530a4e89f 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>
2021-06-27 16:29:41 -04:00

157 lines
6.5 KiB
Python

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