Compare commits

..

20 Commits

Author SHA1 Message Date
RemixDev be131d914f
Removed saveDownloadQueue and tagsLanguage from lib settings 2021-06-27 19:53:41 +02:00
RemixDev 9827cfddb0
Revert embedded cover change 2021-06-16 15:27:22 +02:00
RemixDev c42eff7f95
Fixed bitrate fallback check 2021-06-13 14:06:17 +02:00
RemixDev f8b835229c
Use overwriteFile setting when downloading embedded covers 2021-06-13 14:06:05 +02:00
RemixDev 5dc923d057
Fixed bitrate fallback not working 2021-06-08 19:39:56 +02:00
RemixDev 97cd903289
Fixed some issues to make the lib work 2021-06-08 19:12:15 +02:00
RemixDev 8b7417f8c4
Implemented spotify plugin back 2021-06-08 19:11:46 +02:00
RemixDev c47e394039
Better handling of albums upcs 2021-06-08 11:16:52 +02:00
RemixDev 261b7adb36
Fixed queue item not cancelling correctly 2021-06-08 11:11:08 +02:00
RemixDev 224a62aad2
Code parity with deemix-js 2021-06-07 20:25:51 +02:00
RemixDev 69c165e2bc
Code cleanup with pylint 2021-04-10 11:53:52 +02:00
RemixDev eda8fd3d13
Even more rework on the library 2021-03-24 17:41:03 +01:00
RemixDev dc6adc7887
More work on the library (WIP) 2021-03-19 15:44:21 +01:00
RemixDev 5ee81ced44
Total rework of the library (WIP) 2021-03-19 14:31:32 +01:00
RemixDev 0f733ceaaa
Some rework done on types 2021-03-13 11:54:01 +01:00
RemixDev b91d2a1af3
Added start queue function 2021-03-07 12:03:50 +01:00
RemixDev 318ad689ea
Made nextitem work on a thread 2021-03-01 19:11:07 +01:00
RemixDev 9c49bf5d23
Removed dz as first parameter 2021-02-28 23:24:04 +01:00
RemixDev bd8a1948d4
Started queuemanager refactoring 2021-02-11 19:05:06 +01:00
RemixDev e47a2863e8
Removed eventlet 2021-02-11 17:30:53 +01:00
22 changed files with 687 additions and 994 deletions

12
.gitignore vendored
View File

@ -26,13 +26,11 @@ yarn-error.log*
*.sw? *.sw?
# Private configs # Private configs
config.py /config.py
test.py /test.py
config
#build files #build files
build /build
*egg-info /*egg-info
updatePyPi.sh updatePyPi.sh
deezer /deezer
.cache

View File

@ -7,10 +7,11 @@ from deemix.itemgen import generateTrackItem, \
generatePlaylistItem, \ generatePlaylistItem, \
generateArtistItem, \ generateArtistItem, \
generateArtistDiscographyItem, \ generateArtistDiscographyItem, \
generateArtistTopItem generateArtistTopItem, \
from deemix.errors import LinkNotRecognized, LinkNotSupported LinkNotRecognized, \
LinkNotSupported
__version__ = "3.6.6" __version__ = "3.0.0"
# Returns the Resolved URL, the Type and the ID # Returns the Resolved URL, the Type and the ID
def parseLink(link): def parseLink(link):

View File

@ -7,21 +7,9 @@ from deezer import TrackFormats
from deemix import generateDownloadObject from deemix import generateDownloadObject
from deemix.settings import load as loadSettings from deemix.settings import load as loadSettings
from deemix.utils import getBitrateNumberFromText, formatListener from deemix.utils import getBitrateNumberFromText
import deemix.utils.localpaths as localpaths import deemix.utils.localpaths as localpaths
from deemix.downloader import Downloader from deemix.downloader import Downloader
from deemix.itemgen import GenerationError
try:
from deemix.plugins.spotify import Spotify
except ImportError:
Spotify = None
class LogListener:
@classmethod
def send(cls, key, value=None):
logString = formatListener(key, value)
if logString: print(logString)
@click.command() @click.command()
@click.option('--portable', is_flag=True, help='Creates the config folder in the same directory where the script is launched') @click.option('--portable', is_flag=True, help='Creates the config folder in the same directory where the script is launched')
@ -34,8 +22,7 @@ def download(url, bitrate, portable, path):
configFolder = localpath / 'config' if portable else localpaths.getConfigFolder() configFolder = localpath / 'config' if portable else localpaths.getConfigFolder()
settings = loadSettings(configFolder) settings = loadSettings(configFolder)
dz = Deezer() dz = Deezer(settings.get('tagsLanguage', ""))
listener = LogListener()
def requestValidArl(): def requestValidArl():
while True: while True:
@ -44,22 +31,12 @@ def download(url, bitrate, portable, path):
return arl return arl
if (configFolder / '.arl').is_file(): if (configFolder / '.arl').is_file():
with open(configFolder / '.arl', 'r', encoding="utf-8") as f: with open(configFolder / '.arl', 'r') as f:
arl = f.readline().rstrip("\n").strip() arl = f.readline().rstrip("\n").strip()
if not dz.login_via_arl(arl): arl = requestValidArl() if not dz.login_via_arl(arl): arl = requestValidArl()
else: arl = requestValidArl() else: arl = requestValidArl()
try: with open(configFolder / '.arl', 'w') as f:
with open(configFolder / '.arl', 'w', encoding="utf-8") as f: f.write(arl)
f.write(arl)
except:
print(f"Error opening {configFolder / '.arl'}, continuing anyway.")
plugins = {}
if Spotify:
plugins = {
"spotify": Spotify(configFolder=configFolder)
}
plugins["spotify"].setup()
def downloadLinks(url, bitrate=None): def downloadLinks(url, bitrate=None):
if not bitrate: bitrate = settings.get("maxBitrate", TrackFormats.MP3_320) if not bitrate: bitrate = settings.get("maxBitrate", TrackFormats.MP3_320)
@ -71,24 +48,9 @@ def download(url, bitrate, portable, path):
else: else:
links.append(link) links.append(link)
downloadObjects = []
for link in links: for link in links:
try: downloadObject = generateDownloadObject(dz, link, bitrate)
downloadObject = generateDownloadObject(dz, link, bitrate, plugins, listener) Downloader(dz, downloadObject, settings).start()
except GenerationError as e:
print(f"{e.link}: {e.message}")
continue
if isinstance(downloadObject, list):
downloadObjects += downloadObject
else:
downloadObjects.append(downloadObject)
for obj in downloadObjects:
if obj.__type__ == "Convertable":
obj = plugins[obj.plugin].convert(dz, obj, settings, listener)
Downloader(dz, obj, settings, listener).start()
if path is not None: if path is not None:
if path == '': path = '.' if path == '': path = '.'
@ -104,7 +66,7 @@ def download(url, bitrate, portable, path):
isfile = False isfile = False
if isfile: if isfile:
filename = url[0] filename = url[0]
with open(filename, encoding="utf-8") as f: with open(filename) as f:
url = f.readlines() url = f.readlines()
downloadLinks(url, bitrate) downloadLinks(url, bitrate)

View File

@ -10,7 +10,6 @@ from deemix.utils.crypto import _md5, _ecbCrypt, _ecbDecrypt, generateBlowfishKe
from deemix.utils import USER_AGENT_HEADER from deemix.utils import USER_AGENT_HEADER
from deemix.types.DownloadObjects import Single from deemix.types.DownloadObjects import Single
from deemix.errors import DownloadCanceled, DownloadEmpty
logger = logging.getLogger('deemix') logger = logging.getLogger('deemix')
@ -41,22 +40,15 @@ def reverseStreamURL(url):
return reverseStreamPath(urlPart) return reverseStreamPath(urlPart)
def streamTrack(outputStream, track, start=0, downloadObject=None, listener=None): def streamTrack(outputStream, track, start=0, downloadObject=None, listener=None):
if downloadObject and downloadObject.isCanceled: raise DownloadCanceled if downloadObject.isCanceled: raise DownloadCanceled
headers= {'User-Agent': USER_AGENT_HEADER} headers= {'User-Agent': USER_AGENT_HEADER}
chunkLength = start chunkLength = start
isCryptedStream = "/mobile/" in track.downloadURL or "/media/" in track.downloadURL
itemData = { itemName = f"[{track.mainArtist.name} - {track.title}]"
'id': track.id,
'title': track.title,
'artist': track.mainArtist.name
}
try: try:
with get(track.downloadURL, headers=headers, stream=True, timeout=10) as request: with get(track.downloadUrl, headers=headers, stream=True, timeout=10) as request:
request.raise_for_status() request.raise_for_status()
if isCryptedStream:
blowfish_key = generateBlowfishKey(str(track.id))
complete = int(request.headers["Content-Length"]) complete = int(request.headers["Content-Length"])
if complete == 0: raise DownloadEmpty if complete == 0: raise DownloadEmpty
@ -65,7 +57,7 @@ def streamTrack(outputStream, track, start=0, downloadObject=None, listener=None
if listener: if listener:
listener.send('downloadInfo', { listener.send('downloadInfo', {
'uuid': downloadObject.uuid, 'uuid': downloadObject.uuid,
'data': itemData, 'itemName': itemName,
'state': "downloading", 'state': "downloading",
'alreadyStarted': True, 'alreadyStarted': True,
'value': responseRange 'value': responseRange
@ -74,23 +66,69 @@ def streamTrack(outputStream, track, start=0, downloadObject=None, listener=None
if listener: if listener:
listener.send('downloadInfo', { listener.send('downloadInfo', {
'uuid': downloadObject.uuid, 'uuid': downloadObject.uuid,
'data': itemData, 'itemName': itemName,
'state': "downloading", 'state': "downloading",
'alreadyStarted': False, 'alreadyStarted': False,
'value': complete 'value': complete
}) })
isStart = True
for chunk in request.iter_content(2048 * 3): for chunk in request.iter_content(2048 * 3):
if isCryptedStream: outputStream.write(chunk)
if len(chunk) >= 2048: chunkLength += len(chunk)
chunk = decryptChunk(blowfish_key, chunk[0:2048]) + chunk[2048:]
if isStart and chunk[0] == 0 and chunk[4:8].decode('utf-8') != "ftyp": if downloadObject:
for i, byte in enumerate(chunk): if isinstance(downloadObject, Single):
if byte != 0: break chunkProgres = (chunkLength / (complete + start)) * 100
chunk = chunk[i:] downloadObject.progressNext = chunkProgres
isStart = False 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) outputStream.write(chunk)
chunkLength += len(chunk) chunkLength += len(chunk)
@ -105,7 +143,14 @@ def streamTrack(outputStream, track, start=0, downloadObject=None, listener=None
downloadObject.updateProgress(listener) downloadObject.updateProgress(listener)
except (SSLError, u3SSLError): except (SSLError, u3SSLError):
streamTrack(outputStream, track, chunkLength, downloadObject, listener) logger.info('%s retrying from byte %s', itemName, chunkLength)
streamCryptedTrack(outputStream, track, chunkLength, downloadObject, listener)
except (RequestsConnectionError, ReadTimeout, ChunkedEncodingError): except (RequestsConnectionError, ReadTimeout, ChunkedEncodingError):
sleep(2) sleep(2)
streamTrack(outputStream, track, start, downloadObject, listener) streamCryptedTrack(outputStream, track, start, downloadObject, listener)
class DownloadCanceled(Exception):
pass
class DownloadEmpty(Exception):
pass

View File

@ -1,6 +1,5 @@
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from time import sleep from time import sleep
import traceback
from os.path import sep as pathSep from os.path import sep as pathSep
from os import makedirs, system as execute from os import makedirs, system as execute
@ -19,17 +18,14 @@ from urllib3.exceptions import SSLError as u3SSLError
from mutagen.flac import FLACNoHeaderError, error as FLACError from mutagen.flac import FLACNoHeaderError, error as FLACError
from deezer import TrackFormats from deezer import TrackFormats
from deezer.errors import WrongLicense, WrongGeolocation
from deezer.utils import map_track
from deemix.types.DownloadObjects import Single, Collection from deemix.types.DownloadObjects import Single, Collection
from deemix.types.Track import Track from deemix.types.Track import Track, AlbumDoesntExists, MD5NotFound
from deemix.types.Picture import StaticPicture from deemix.types.Picture import StaticPicture
from deemix.utils import USER_AGENT_HEADER from deemix.utils import USER_AGENT_HEADER
from deemix.utils.pathtemplates import generatePath, generateAlbumName, generateArtistName, generateDownloadObjectName from deemix.utils.pathtemplates import generatePath, generateAlbumName, generateArtistName, generateDownloadObjectName
from deemix.tagger import tagID3, tagFLAC from deemix.tagger import tagID3, tagFLAC
from deemix.decryption import generateCryptedStreamURL, streamTrack from deemix.decryption import generateStreamURL, streamTrack, DownloadCanceled
from deemix.settings import OverwriteOption from deemix.settings import OverwriteOption
from deemix.errors import DownloadFailed, MD5NotFound, DownloadCanceled, PreferredBitrateNotFound, TrackNot360, AlbumDoesntExists, DownloadError, ErrorMessages
logger = logging.getLogger('deemix') logger = logging.getLogger('deemix')
@ -44,17 +40,6 @@ extensions = {
TrackFormats.MP4_RA1: '.mp4' TrackFormats.MP4_RA1: '.mp4'
} }
formatsName = {
TrackFormats.FLAC: 'FLAC',
TrackFormats.LOCAL: 'MP3_MISC',
TrackFormats.MP3_320: 'MP3_320',
TrackFormats.MP3_128: 'MP3_128',
TrackFormats.DEFAULT: 'MP3_MISC',
TrackFormats.MP4_RA3: 'MP4_RA3',
TrackFormats.MP4_RA2: 'MP4_RA2',
TrackFormats.MP4_RA1: 'MP4_RA1'
}
TEMPDIR = Path(gettempdir()) / 'deemix-imgs' TEMPDIR = Path(gettempdir()) / 'deemix-imgs'
if not TEMPDIR.is_dir(): makedirs(TEMPDIR) if not TEMPDIR.is_dir(): makedirs(TEMPDIR)
@ -75,7 +60,7 @@ def downloadImage(url, path, overwrite=OverwriteOption.DONT_OVERWRITE):
pictureSize = int(pictureUrl[:pictureUrl.find("x")]) pictureSize = int(pictureUrl[:pictureUrl.find("x")])
if pictureSize > 1200: if pictureSize > 1200:
return downloadImage(urlBase+pictureUrl.replace(f"{pictureSize}x{pictureSize}", '1200x1200'), path, overwrite) return downloadImage(urlBase+pictureUrl.replace(f"{pictureSize}x{pictureSize}", '1200x1200'), path, overwrite)
except (requests.exceptions.ConnectionError, requests.exceptions.ChunkedEncodingError, u3SSLError): except (requests.exceptions.ConnectionError, requests.exceptions.ChunkedEncodingError, u3SSLError) as e:
if path.is_file(): path.unlink() if path.is_file(): path.unlink()
sleep(5) sleep(5)
return downloadImage(url, path, overwrite) return downloadImage(url, path, overwrite)
@ -85,54 +70,11 @@ def downloadImage(url, path, overwrite=OverwriteOption.DONT_OVERWRITE):
logger.exception("Error while downloading an image, you should report this to the developers: %s", e) logger.exception("Error while downloading an image, you should report this to the developers: %s", e)
return None return None
def getPreferredBitrate(dz, track, preferredBitrate, shouldFallback, feelingLucky, uuid=None, listener=None): def getPreferredBitrate(track, bitrate, shouldFallback, uuid=None, listener=None):
preferredBitrate = int(preferredBitrate) bitrate = int(bitrate)
if track.local: return TrackFormats.LOCAL
falledBack = False falledBack = False
hasAlternative = track.fallbackID != "0"
isGeolocked = False
wrongLicense = False
def testURL(track, url, formatName):
if not url: return False
request = requests.head(
url,
headers={'User-Agent': USER_AGENT_HEADER},
timeout=30
)
try:
request.raise_for_status()
track.filesizes[f"{formatName.lower()}"] = int(request.headers["Content-Length"])
track.filesizes[f"{formatName.lower()}_TESTED"] = True
return track.filesizes[f"{formatName.lower()}"] != 0
except requests.exceptions.HTTPError: # if the format is not available, Deezer returns a 403 error
return False
def getCorrectURL(track, formatName, formatNumber, feelingLucky):
nonlocal wrongLicense, isGeolocked
url = None
# Check the track with the legit method
wrongLicense = (
(formatName == "FLAC" or formatName.startswith("MP4_RA")) and not dz.current_user.get('can_stream_lossless') or \
formatName == "MP3_320" and not dz.current_user.get('can_stream_hq')
)
if track.filesizes.get(formatName.lower()) and track.filesizes[formatName.lower()] != "0":
try:
url = dz.get_track_url(track.trackToken, formatName)
except (WrongLicense, WrongGeolocation) as e:
wrongLicense = isinstance(e, WrongLicense)
isGeolocked = isinstance(e, WrongGeolocation)
# Fallback to old method
if not url and feelingLucky:
url = generateCryptedStreamURL(track.id, track.MD5, track.mediaVersion, formatNumber)
if testURL(track, url, formatName): return url
url = None
return url
if track.local:
url = getCorrectURL(track, "MP3_MISC", TrackFormats.LOCAL, feelingLucky)
track.urls["MP3_MISC"] = url
return TrackFormats.LOCAL
formats_non_360 = { formats_non_360 = {
TrackFormats.FLAC: "FLAC", TrackFormats.FLAC: "FLAC",
@ -145,7 +87,8 @@ def getPreferredBitrate(dz, track, preferredBitrate, shouldFallback, feelingLuck
TrackFormats.MP4_RA1: "MP4_RA1", TrackFormats.MP4_RA1: "MP4_RA1",
} }
is360format = preferredBitrate in formats_360.keys() is360format = bitrate in formats_360.keys()
if not shouldFallback: if not shouldFallback:
formats = formats_360 formats = formats_360
formats.update(formats_non_360) formats.update(formats_non_360)
@ -154,41 +97,38 @@ def getPreferredBitrate(dz, track, preferredBitrate, shouldFallback, feelingLuck
else: else:
formats = formats_non_360 formats = formats_non_360
# check and renew trackToken before starting the check def testBitrate(track, formatNumber, formatName):
track.checkAndRenewTrackToken(dz) request = requests.head(
for formatNumber, formatName in formats.items(): generateStreamURL(track.id, track.MD5, track.mediaVersion, formatNumber),
# Current bitrate is higher than preferred bitrate; skip headers={'User-Agent': USER_AGENT_HEADER},
if formatNumber > preferredBitrate: continue timeout=30
)
currentTrack = track try:
url = getCorrectURL(currentTrack, formatName, formatNumber, feelingLucky) request.raise_for_status()
newTrack = None track.filesizes[f"FILESIZE_{formatName}"] = int(request.headers["Content-Length"])
while True: track.filesizes[f"FILESIZE_{formatName}_TESTED"] = True
if not url and hasAlternative: if track.filesizes[f"FILESIZE_{formatName}"] == 0: return None
newTrack = dz.gw.get_track_with_fallback(currentTrack.fallbackID)
newTrack = map_track(newTrack)
currentTrack = Track()
currentTrack.parseEssentialData(newTrack)
hasAlternative = currentTrack.fallbackID != "0"
if not url: getCorrectURL(currentTrack, formatName, formatNumber, feelingLucky)
if (url or not hasAlternative): break
if url:
if newTrack: track.parseEssentialData(newTrack)
track.urls[formatName] = url
return formatNumber return formatNumber
except requests.exceptions.HTTPError: # if the format is not available, Deezer returns a 403 error
return None
for formatNumber, formatName in formats.items():
if formatNumber > bitrate: continue
if f"FILESIZE_{formatName}" in track.filesizes:
if int(track.filesizes[f"FILESIZE_{formatName}"]) != 0: return formatNumber
if not track.filesizes[f"FILESIZE_{formatName}_TESTED"]:
testedBitrate = testBitrate(track, formatNumber, formatName)
if testedBitrate: return testedBitrate
if not shouldFallback: if not shouldFallback:
if wrongLicense: raise WrongLicense(formatName)
if isGeolocked: raise WrongGeolocation(dz.current_user['country'])
raise PreferredBitrateNotFound raise PreferredBitrateNotFound
if not falledBack: if not falledBack:
falledBack = True falledBack = True
logger.info("%s Fallback to lower bitrate", f"[{track.mainArtist.name} - {track.title}]") logger.info("%s Fallback to lower bitrate", f"[{track.mainArtist.name} - {track.title}]")
if listener and uuid: if listener and uuid:
listener.send('downloadInfo', { listener.send('queueUpdate', {
'uuid': uuid, 'uuid': uuid,
'state': 'bitrateFallback', 'bitrateFallback': True,
'data': { 'data': {
'id': track.id, 'id': track.id,
'title': track.title, 'title': track.title,
@ -196,8 +136,6 @@ def getPreferredBitrate(dz, track, preferredBitrate, shouldFallback, feelingLuck
}, },
}) })
if is360format: raise TrackNot360 if is360format: raise TrackNot360
url = getCorrectURL(track, "MP3_MISC", TrackFormats.DEFAULT, feelingLucky)
track.urls["MP3_MISC"] = url
return TrackFormats.DEFAULT return TrackFormats.DEFAULT
class Downloader: class Downloader:
@ -208,6 +146,7 @@ class Downloader:
self.bitrate = downloadObject.bitrate self.bitrate = downloadObject.bitrate
self.listener = listener self.listener = listener
self.extrasPath = None
self.playlistCoverName = None self.playlistCoverName = None
self.playlistURLs = [] self.playlistURLs = []
@ -215,58 +154,47 @@ class Downloader:
if not self.downloadObject.isCanceled: if not self.downloadObject.isCanceled:
if isinstance(self.downloadObject, Single): if isinstance(self.downloadObject, Single):
track = self.downloadWrapper({ track = self.downloadWrapper({
'trackAPI_gw': self.downloadObject.single['trackAPI_gw'],
'trackAPI': self.downloadObject.single.get('trackAPI'), 'trackAPI': self.downloadObject.single.get('trackAPI'),
'albumAPI': self.downloadObject.single.get('albumAPI') 'albumAPI': self.downloadObject.single.get('albumAPI')
}) })
if track: self.afterDownloadSingle(track) if track: self.afterDownloadSingle(track)
elif isinstance(self.downloadObject, Collection): elif isinstance(self.downloadObject, Collection):
tracks = [None] * len(self.downloadObject.collection['tracks']) tracks = [None] * len(self.downloadObject.collection['tracks_gw'])
with ThreadPoolExecutor(self.settings['queueConcurrency']) as executor: with ThreadPoolExecutor(self.settings['queueConcurrency']) as executor:
for pos, track in enumerate(self.downloadObject.collection['tracks'], start=0): for pos, track in enumerate(self.downloadObject.collection['tracks_gw'], start=0):
tracks[pos] = executor.submit(self.downloadWrapper, { tracks[pos] = executor.submit(self.downloadWrapper, {
'trackAPI': track, 'trackAPI_gw': track,
'albumAPI': self.downloadObject.collection.get('albumAPI'), 'albumAPI': self.downloadObject.collection.get('albumAPI'),
'playlistAPI': self.downloadObject.collection.get('playlistAPI') 'playlistAPI': self.downloadObject.collection.get('playlistAPI')
}) })
self.afterDownloadCollection(tracks) self.afterDownloadCollection(tracks)
if self.listener: if self.listener:
if self.downloadObject.isCanceled: if self.listener:
self.listener.send('currentItemCancelled', self.downloadObject.uuid) self.listener.send('currentItemCancelled', self.downloadObject.uuid)
self.listener.send("removedFromQueue", self.downloadObject.uuid) self.listener.send("removedFromQueue", self.downloadObject.uuid)
else: else:
self.listener.send("finishDownload", self.downloadObject.uuid) self.listener.send("finishDownload", self.downloadObject.uuid)
def log(self, data, state):
if self.listener:
self.listener.send('downloadInfo', {'uuid': self.downloadObject.uuid, 'data': data, 'state': state})
def warn(self, data, state, solution):
if self.listener:
self.listener.send('downloadWarn', {'uuid': self.downloadObject.uuid, 'data': data, 'state': state, 'solution': solution})
def download(self, extraData, track=None): def download(self, extraData, track=None):
returnData = {} returnData = {}
trackAPI_gw = extraData['trackAPI_gw']
trackAPI = extraData.get('trackAPI') trackAPI = extraData.get('trackAPI')
albumAPI = extraData.get('albumAPI') albumAPI = extraData.get('albumAPI')
playlistAPI = extraData.get('playlistAPI') playlistAPI = extraData.get('playlistAPI')
trackAPI['size'] = self.downloadObject.size
if self.downloadObject.isCanceled: raise DownloadCanceled if self.downloadObject.isCanceled: raise DownloadCanceled
if int(trackAPI['id']) == 0: raise DownloadFailed("notOnDeezer") if trackAPI_gw['SNG_ID'] == "0": raise DownloadFailed("notOnDeezer")
itemData = { itemName = f"[{trackAPI_gw['ART_NAME']} - {trackAPI_gw['SNG_TITLE']}]"
'id': trackAPI['id'],
'title': trackAPI['title'],
'artist': trackAPI['artist']['name']
}
# Create Track object # Create Track object
if not track: if not track:
self.log(itemData, "getTags") logger.info("%s Getting the tags", itemName)
try: try:
track = Track().parseData( track = Track().parseData(
dz=self.dz, dz=self.dz,
track_id=trackAPI['id'], trackAPI_gw=trackAPI_gw,
trackAPI=trackAPI, trackAPI=trackAPI,
albumAPI=albumAPI, albumAPI=albumAPI,
playlistAPI=playlistAPI playlistAPI=playlistAPI
@ -275,38 +203,26 @@ class Downloader:
raise DownloadError('albumDoesntExists') from e raise DownloadError('albumDoesntExists') from e
except MD5NotFound as e: except MD5NotFound as e:
raise DownloadError('notLoggedIn') from e raise DownloadError('notLoggedIn') from e
self.log(itemData, "gotTags")
itemData = { itemName = f"[{track.mainArtist.name} - {track.title}]"
'id': track.id,
'title': track.title,
'artist': track.mainArtist.name
}
# Check if track not yet encoded # Check if track not yet encoded
if track.MD5 == '': raise DownloadFailed("notEncoded", track) if track.MD5 == '': raise DownloadFailed("notEncoded", track)
# Choose the target bitrate # Choose the target bitrate
self.log(itemData, "getBitrate")
try: try:
selectedFormat = getPreferredBitrate( selectedFormat = getPreferredBitrate(
self.dz,
track, track,
self.bitrate, self.bitrate,
self.settings['fallbackBitrate'], self.settings['feelingLucky'], self.settings['fallbackBitrate'],
self.downloadObject.uuid, self.listener self.downloadObject.uuid, self.listener
) )
except WrongLicense as e:
raise DownloadFailed("wrongLicense") from e
except WrongGeolocation as e:
raise DownloadFailed("wrongGeolocation", track) from e
except PreferredBitrateNotFound as e: except PreferredBitrateNotFound as e:
raise DownloadFailed("wrongBitrate", track) from e raise DownloadFailed("wrongBitrate", track) from e
except TrackNot360 as e: except TrackNot360 as e:
raise DownloadFailed("no360RA") from e raise DownloadFailed("no360RA") from e
track.bitrate = selectedFormat track.bitrate = selectedFormat
track.album.bitrate = selectedFormat track.album.bitrate = selectedFormat
self.log(itemData, "gotBitrate")
# Apply settings # Apply settings
track.applySettings(self.settings) track.applySettings(self.settings)
@ -320,7 +236,7 @@ class Downloader:
writepath = filepath / f"{filename}{extension}" writepath = filepath / f"{filename}{extension}"
# Save extrasPath # Save extrasPath
if extrasPath and not self.downloadObject.extrasPath: self.downloadObject.extrasPath = extrasPath if extrasPath and not self.extrasPath: self.extrasPath = extrasPath
# Generate covers URLs # Generate covers URLs
embeddedImageFormat = f'jpg-{self.settings["jpegImageQuality"]}' embeddedImageFormat = f'jpg-{self.settings["jpegImageQuality"]}'
@ -332,9 +248,8 @@ class Downloader:
track.album.embeddedCoverPath = TEMPDIR / ((f"pl{track.playlist.id}" if track.album.isPlaylist else f"alb{track.album.id}") + f"_{self.settings['embeddedArtworkSize']}{ext}") track.album.embeddedCoverPath = TEMPDIR / ((f"pl{track.playlist.id}" if track.album.isPlaylist else f"alb{track.album.id}") + f"_{self.settings['embeddedArtworkSize']}{ext}")
# Download and cache coverart # Download and cache coverart
self.log(itemData, "getAlbumArt") logger.info("%s Getting the album cover", itemName)
track.album.embeddedCoverPath = downloadImage(track.album.embeddedCoverURL, track.album.embeddedCoverPath) track.album.embeddedCoverPath = downloadImage(track.album.embeddedCoverURL, track.album.embeddedCoverPath)
self.log(itemData, "gotAlbumArt")
# Save local album art # Save local album art
if coverPath: if coverPath:
@ -382,8 +297,8 @@ class Downloader:
# Save lyrics in lrc file # Save lyrics in lrc file
if self.settings['syncedLyrics'] and track.lyrics.sync: if self.settings['syncedLyrics'] and track.lyrics.sync:
if not (filepath / f"{filename}.lrc").is_file() or self.settings['overwriteFile'] in [OverwriteOption.OVERWRITE, OverwriteOption.ONLY_TAGS]: if not (filepath / f"{filename}.lrc").is_file() or self.settings['overwriteFile'] in [OverwriteOption.OVERWRITE, OverwriteOption.ONLY_TAGS]:
with open(filepath / f"{filename}.lrc", 'w', encoding="utf-8") as f: with open(filepath / f"{filename}.lrc", 'wb') as f:
f.write(track.lyrics.sync) f.write(track.lyrics.sync.encode('utf-8'))
# Check for overwrite settings # Check for overwrite settings
trackAlreadyDownloaded = writepath.is_file() trackAlreadyDownloaded = writepath.is_file()
@ -407,26 +322,26 @@ class Downloader:
writepath = Path(currentFilename) writepath = Path(currentFilename)
if not trackAlreadyDownloaded or self.settings['overwriteFile'] == OverwriteOption.OVERWRITE: if not trackAlreadyDownloaded or self.settings['overwriteFile'] == OverwriteOption.OVERWRITE:
track.downloadURL = track.urls[formatsName[track.bitrate]] logger.info("%s Downloading the track", itemName)
if not track.downloadURL: raise DownloadFailed('notAvailable', track) track.downloadUrl = generateStreamURL(track.id, track.MD5, track.mediaVersion, track.bitrate)
try: try:
with open(writepath, 'wb') as stream: with open(writepath, 'wb') as stream:
streamTrack(stream, track, downloadObject=self.downloadObject, listener=self.listener) streamTrack(stream, track, downloadObject=self.downloadObject, listener=self.listener)
except requests.exceptions.HTTPError as e: except requests.exceptions.HTTPError as e:
if writepath.is_file(): writepath.unlink()
raise DownloadFailed('notAvailable', track) from e raise DownloadFailed('notAvailable', track) from e
except OSError as e: except OSError as e:
if writepath.is_file(): writepath.unlink() if writepath.is_file(): writepath.unlink()
if e.errno == errno.ENOSPC: raise DownloadFailed("noSpaceLeft") from e if e.errno == errno.ENOSPC: raise DownloadFailed("noSpaceLeft") from e
raise e raise e
self.log(itemData, "downloaded")
else: else:
self.log(itemData, "alreadyDownloaded") logger.info("%s Skipping track as it's already downloaded", itemName)
self.downloadObject.completeTrackProgress(self.listener) self.downloadObject.completeTrackProgress(self.listener)
# Adding tags # Adding tags
if (not trackAlreadyDownloaded or self.settings['overwriteFile'] in [OverwriteOption.ONLY_TAGS, OverwriteOption.OVERWRITE]) and not track.local: if (not trackAlreadyDownloaded or self.settings['overwriteFile'] in [OverwriteOption.ONLY_TAGS, OverwriteOption.OVERWRITE]) and not track.local:
self.log(itemData, "tagging") logger.info("%s Applying tags to the track", itemName)
if extension == '.mp3': if extension == '.mp3':
tagID3(writepath, track, self.settings['tags']) tagID3(writepath, track, self.settings['tags'])
elif extension == '.flac': elif extension == '.flac':
@ -434,35 +349,47 @@ class Downloader:
tagFLAC(writepath, track, self.settings['tags']) tagFLAC(writepath, track, self.settings['tags'])
except (FLACNoHeaderError, FLACError): except (FLACNoHeaderError, FLACError):
writepath.unlink() writepath.unlink()
logger.warning("%s Track not available in FLAC, falling back if necessary", f"{itemData['artist']} - {itemData['title']}") logger.warning("%s Track not available in FLAC, falling back if necessary", itemName)
self.downloadObject.removeTrackProgress(self.listener) self.downloadObject.removeTrackProgress(self.listener)
track.filesizes['FILESIZE_FLAC'] = "0" track.filesizes['FILESIZE_FLAC'] = "0"
track.filesizes['FILESIZE_FLAC_TESTED'] = True track.filesizes['FILESIZE_FLAC_TESTED'] = True
return self.download(extraData, track=track) return self.download(trackAPI_gw, track=track)
self.log(itemData, "tagged")
if track.searched: returnData['searched'] = True if track.searched: returnData['searched'] = True
self.downloadObject.downloaded += 1 self.downloadObject.downloaded += 1
self.downloadObject.files.append(str(writepath))
self.downloadObject.extrasPath = str(self.extrasPath)
logger.info("%s Track download completed\n%s", itemName, writepath)
if self.listener: self.listener.send("updateQueue", { if self.listener: self.listener.send("updateQueue", {
'uuid': self.downloadObject.uuid, 'uuid': self.downloadObject.uuid,
'downloaded': True, 'downloaded': True,
'downloadPath': str(writepath), 'downloadPath': str(writepath),
'extrasPath': str(self.downloadObject.extrasPath) 'extrasPath': str(self.extrasPath)
}) })
returnData['filename'] = str(writepath)[len(str(extrasPath))+ len(pathSep):] returnData['filename'] = str(writepath)[len(str(extrasPath))+ len(pathSep):]
returnData['data'] = itemData returnData['data'] = {
returnData['path'] = str(writepath) 'id': track.id,
self.downloadObject.files.append(returnData) 'title': track.title,
'artist': track.mainArtist.name
}
return returnData return returnData
def downloadWrapper(self, extraData, track=None): def downloadWrapper(self, extraData, track=None):
trackAPI = extraData['trackAPI'] trackAPI_gw = extraData['trackAPI_gw']
if ('_EXTRA_TRACK' in trackAPI_gw):
extraData['trackAPI'] = trackAPI_gw['_EXTRA_TRACK'].copy()
del extraData['trackAPI_gw']['_EXTRA_TRACK']
del trackAPI_gw['_EXTRA_TRACK']
# Temp metadata to generate logs # Temp metadata to generate logs
itemData = { tempTrack = {
'id': trackAPI['id'], 'id': trackAPI_gw['SNG_ID'],
'title': trackAPI['title'], 'title': trackAPI_gw['SNG_TITLE'].strip(),
'artist': trackAPI['artist']['name'] 'artist': trackAPI_gw['ART_NAME']
} }
if trackAPI_gw.get('VERSION') and trackAPI_gw['VERSION'] not in trackAPI_gw['SNG_TITLE']:
tempTrack['title'] += f" {trackAPI_gw['VERSION']}".strip()
itemName = f"[{tempTrack['artist']} - {tempTrack['title']}]"
try: try:
result = self.download(extraData, track) result = self.download(extraData, track)
@ -470,50 +397,42 @@ class Downloader:
if error.track: if error.track:
track = error.track track = error.track
if track.fallbackID != "0": if track.fallbackID != "0":
self.warn(itemData, error.errid, 'fallback') logger.warning("%s %s Using fallback id", itemName, error.message)
newTrack = self.dz.gw.get_track_with_fallback(track.fallbackID) newTrack = self.dz.gw.get_track_with_fallback(track.fallbackID)
newTrack = map_track(newTrack)
track.parseEssentialData(newTrack) track.parseEssentialData(newTrack)
track.retriveFilesizes(self.dz)
return self.downloadWrapper(extraData, track) return self.downloadWrapper(extraData, track)
if len(track.albumsFallback) != 0 and self.settings['fallbackISRC']:
newAlbumID = track.albumsFallback.pop()
newAlbum = self.dz.gw.get_album_page(newAlbumID)
fallbackID = 0
for newTrack in newAlbum['SONGS']['data']:
if newTrack['ISRC'] == track.ISRC:
fallbackID = newTrack['SNG_ID']
break
if fallbackID != 0:
self.warn(itemData, error.errid, 'fallback')
newTrack = self.dz.gw.get_track_with_fallback(fallbackID)
newTrack = map_track(newTrack)
track.parseEssentialData(newTrack)
return self.downloadWrapper(extraData, track)
if not track.searched and self.settings['fallbackSearch']: if not track.searched and self.settings['fallbackSearch']:
self.warn(itemData, error.errid, 'search') logger.warning("%s %s Searching for alternative", itemName, error.message)
searchedId = self.dz.api.get_track_id_from_metadata(track.mainArtist.name, track.title, track.album.title) searchedId = self.dz.api.get_track_id_from_metadata(track.mainArtist.name, track.title, track.album.title)
if searchedId != "0": if searchedId != "0":
newTrack = self.dz.gw.get_track_with_fallback(searchedId) newTrack = self.dz.gw.get_track_with_fallback(searchedId)
newTrack = map_track(newTrack)
track.parseEssentialData(newTrack) track.parseEssentialData(newTrack)
track.retriveFilesizes(self.dz)
track.searched = True track.searched = True
self.log(itemData, "searchFallback") if self.listener: self.listener.send('queueUpdate', {
'uuid': self.downloadObject.uuid,
'searchFallback': True,
'data': {
'id': track.id,
'title': track.title,
'artist': track.mainArtist.name
},
})
return self.downloadWrapper(extraData, track) return self.downloadWrapper(extraData, track)
error.errid += "NoAlternative" error.errid += "NoAlternative"
error.message = ErrorMessages[error.errid] error.message = errorMessages[error.errid]
logger.error("%s %s", itemName, error.message)
result = {'error': { result = {'error': {
'message': error.message, 'message': error.message,
'errid': error.errid, 'errid': error.errid,
'data': itemData, 'data': tempTrack
'type': "track"
}} }}
except Exception as e: except Exception as e:
logger.exception("%s %s", f"{itemData['artist']} - {itemData['title']}", e) logger.exception("%s %s", itemName, e)
result = {'error': { result = {'error': {
'message': str(e), 'message': str(e),
'data': itemData, 'data': tempTrack
'stack': traceback.format_exc(),
'type': "track"
}} }}
if 'error' in result: if 'error' in result:
@ -527,74 +446,39 @@ class Downloader:
'failed': True, 'failed': True,
'data': error['data'], 'data': error['data'],
'error': error['message'], 'error': error['message'],
'errid': error.get('errid'), 'errid': error['errid'] if 'errid' in error else None
'stack': error.get('stack'),
'type': error['type']
}) })
return result return result
def afterDownloadErrorReport(self, position, error, itemData=None):
if not itemData: itemData = {}
data = {'position': position }
data.update(itemData)
logger.exception("%s %s", position, error)
self.downloadObject.errors.append({
'message': str(error),
'stack': traceback.format_exc(),
'data': data,
'type': "post"
})
if self.listener:
self.listener.send("updateQueue", {
'uuid': self.downloadObject.uuid,
'postFailed': True,
'data': data,
'error': str(error),
'stack': traceback.format_exc(),
'type': "post"
})
def afterDownloadSingle(self, track): def afterDownloadSingle(self, track):
if not self.downloadObject.extrasPath: self.downloadObject.extrasPath = Path(self.settings['downloadLocation']) if not self.extrasPath: self.extrasPath = Path(self.settings['downloadLocation'])
# Save Album Cover # Save Album Cover
try: if self.settings['saveArtwork'] and 'albumPath' in track:
if self.settings['saveArtwork'] and 'albumPath' in track: for image in track['albumURLs']:
for image in track['albumURLs']: downloadImage(image['url'], track['albumPath'] / f"{track['albumFilename']}.{image['ext']}", self.settings['overwriteFile'])
downloadImage(image['url'], track['albumPath'] / f"{track['albumFilename']}.{image['ext']}", self.settings['overwriteFile'])
except Exception as e:
self.afterDownloadErrorReport("SaveLocalAlbumArt", e)
# Save Artist Artwork # Save Artist Artwork
try: if self.settings['saveArtworkArtist'] and 'artistPath' in track:
if self.settings['saveArtworkArtist'] and 'artistPath' in track: for image in track['artistURLs']:
for image in track['artistURLs']: downloadImage(image['url'], track['artistPath'] / f"{track['artistFilename']}.{image['ext']}", self.settings['overwriteFile'])
downloadImage(image['url'], track['artistPath'] / f"{track['artistFilename']}.{image['ext']}", self.settings['overwriteFile'])
except Exception as e:
self.afterDownloadErrorReport("SaveLocalArtistArt", e)
# Create searched logfile # Create searched logfile
try: if self.settings['logSearched'] and 'searched' in track:
if self.settings['logSearched'] and 'searched' in track: filename = f"{track.data.artist} - {track.data.title}"
filename = f"{track.data.artist} - {track.data.title}" with open(self.extrasPath / 'searched.txt', 'wb+') as f:
with open(self.downloadObject.extrasPath / 'searched.txt', 'w+', encoding="utf-8") as f: searchedFile = f.read().decode('utf-8')
searchedFile = f.read() if not filename in searchedFile:
if not filename in searchedFile: if searchedFile != "": searchedFile += "\r\n"
if searchedFile != "": searchedFile += "\r\n" searchedFile += filename + "\r\n"
searchedFile += filename + "\r\n" f.write(searchedFile.encode('utf-8'))
f.write(searchedFile)
except Exception as e:
self.afterDownloadErrorReport("CreateSearchedLog", e)
# Execute command after download # Execute command after download
try: if self.settings['executeCommand'] != "":
if self.settings['executeCommand'] != "": execute(self.settings['executeCommand'].replace("%folder%", quote(str(self.extrasPath))).replace("%filename%", quote(track['filename'])), shell=True)
execute(self.settings['executeCommand'].replace("%folder%", quote(str(self.downloadObject.extrasPath))).replace("%filename%", quote(track['filename'])))
except Exception as e:
self.afterDownloadErrorReport("ExecuteCommand", e)
def afterDownloadCollection(self, tracks): def afterDownloadCollection(self, tracks):
if not self.downloadObject.extrasPath: self.downloadObject.extrasPath = Path(self.settings['downloadLocation']) if not self.extrasPath: self.extrasPath = Path(self.settings['downloadLocation'])
playlist = [None] * len(tracks) playlist = [None] * len(tracks)
errors = "" errors = ""
searched = "" searched = ""
@ -612,61 +496,69 @@ class Downloader:
if 'searched' in track: searched += track['searched'] + "\r\n" if 'searched' in track: searched += track['searched'] + "\r\n"
# Save Album Cover # Save Album Cover
try: if self.settings['saveArtwork'] and 'albumPath' in track:
if self.settings['saveArtwork'] and 'albumPath' in track: for image in track['albumURLs']:
for image in track['albumURLs']: downloadImage(image['url'], track['albumPath'] / f"{track['albumFilename']}.{image['ext']}", self.settings['overwriteFile'])
downloadImage(image['url'], track['albumPath'] / f"{track['albumFilename']}.{image['ext']}", self.settings['overwriteFile'])
except Exception as e:
self.afterDownloadErrorReport("SaveLocalAlbumArt", e, track['data'])
# Save Artist Artwork # Save Artist Artwork
try: if self.settings['saveArtworkArtist'] and 'artistPath' in track:
if self.settings['saveArtworkArtist'] and 'artistPath' in track: for image in track['artistURLs']:
for image in track['artistURLs']: downloadImage(image['url'], track['artistPath'] / f"{track['artistFilename']}.{image['ext']}", self.settings['overwriteFile'])
downloadImage(image['url'], track['artistPath'] / f"{track['artistFilename']}.{image['ext']}", self.settings['overwriteFile'])
except Exception as e:
self.afterDownloadErrorReport("SaveLocalArtistArt", e, track['data'])
# Save filename for playlist file # Save filename for playlist file
playlist[i] = track.get('filename', "") playlist[i] = track.get('filename', "")
# Create errors logfile # Create errors logfile
try: if self.settings['logErrors'] and errors != "":
if self.settings['logErrors'] and errors != "": with open(self.extrasPath / 'errors.txt', 'wb') as f:
with open(self.downloadObject.extrasPath / 'errors.txt', 'w', encoding="utf-8") as f: f.write(errors.encode('utf-8'))
f.write(errors)
except Exception as e:
self.afterDownloadErrorReport("CreateErrorLog", e)
# Create searched logfile # Create searched logfile
try: if self.settings['logSearched'] and searched != "":
if self.settings['logSearched'] and searched != "": with open(self.extrasPath / 'searched.txt', 'wb') as f:
with open(self.downloadObject.extrasPath / 'searched.txt', 'w', encoding="utf-8") as f: f.write(searched.encode('utf-8'))
f.write(searched)
except Exception as e:
self.afterDownloadErrorReport("CreateSearchedLog", e)
# Save Playlist Artwork # Save Playlist Artwork
try: if self.settings['saveArtwork'] and self.playlistCoverName and not self.settings['tags']['savePlaylistAsCompilation']:
if self.settings['saveArtwork'] and self.playlistCoverName and not self.settings['tags']['savePlaylistAsCompilation']: for image in self.playlistURLs:
for image in self.playlistURLs: downloadImage(image['url'], self.extrasPath / f"{self.playlistCoverName}.{image['ext']}", self.settings['overwriteFile'])
downloadImage(image['url'], self.downloadObject.extrasPath / f"{self.playlistCoverName}.{image['ext']}", self.settings['overwriteFile'])
except Exception as e:
self.afterDownloadErrorReport("SavePlaylistArt", e)
# Create M3U8 File # Create M3U8 File
try: if self.settings['createM3U8File']:
if self.settings['createM3U8File']: filename = generateDownloadObjectName(self.settings['playlistFilenameTemplate'], self.downloadObject, self.settings) or "playlist"
filename = generateDownloadObjectName(self.settings['playlistFilenameTemplate'], self.downloadObject, self.settings) or "playlist" with open(self.extrasPath / f'{filename}.m3u8', 'wb') as f:
with open(self.downloadObject.extrasPath / f'{filename}.m3u8', 'w', encoding="utf-8") as f: for line in playlist:
for line in playlist: f.write((line + "\n").encode('utf-8'))
f.write(line + "\n")
except Exception as e:
self.afterDownloadErrorReport("CreatePlaylistFile", e)
# Execute command after download # Execute command after download
try: if self.settings['executeCommand'] != "":
if self.settings['executeCommand'] != "": execute(self.settings['executeCommand'].replace("%folder%", quote(str(self.extrasPath))), shell=True)
execute(self.settings['executeCommand'].replace("%folder%", quote(str(self.downloadObject.extrasPath))))
except Exception as e: class DownloadError(Exception):
self.afterDownloadErrorReport("ExecuteCommand", e) """Base class for exceptions in this module."""
errorMessages = {
'notOnDeezer': "Track not available on Deezer!",
'notEncoded': "Track not yet encoded!",
'notEncodedNoAlternative': "Track not yet encoded and no alternative found!",
'wrongBitrate': "Track not found at desired bitrate.",
'wrongBitrateNoAlternative': "Track not found at desired bitrate and no alternative found!",
'no360RA': "Track is not available in Reality Audio 360.",
'notAvailable': "Track not available on deezer's servers!",
'notAvailableNoAlternative': "Track not available on deezer's servers and no alternative found!",
'noSpaceLeft': "No space left on target drive, clean up some space for the tracks",
'albumDoesntExists': "Track's album does not exsist, failed to gather info"
}
class DownloadFailed(DownloadError):
def __init__(self, errid, track=None):
super().__init__()
self.errid = errid
self.message = errorMessages[self.errid]
self.track = track
class PreferredBitrateNotFound(DownloadError):
pass
class TrackNot360(DownloadError):
pass

View File

@ -1,96 +0,0 @@
class DeemixError(Exception):
"""Base exception for this module"""
class GenerationError(DeemixError):
"""Generation related errors"""
def __init__(self, link, message, errid=None):
super().__init__()
self.link = link
self.message = message
self.errid = errid
def toDict(self):
return {
'link': self.link,
'error': self.message,
'errid': self.errid
}
class ISRCnotOnDeezer(GenerationError):
def __init__(self, link):
super().__init__(link, "Track ISRC is not available on deezer", "ISRCnotOnDeezer")
class NotYourPrivatePlaylist(GenerationError):
def __init__(self, link):
super().__init__(link, "You can't download others private playlists.", "notYourPrivatePlaylist")
class TrackNotOnDeezer(GenerationError):
def __init__(self, link):
super().__init__(link, "Track not found on deezer!", "trackNotOnDeezer")
class AlbumNotOnDeezer(GenerationError):
def __init__(self, link):
super().__init__(link, "Album not found on deezer!", "albumNotOnDeezer")
class InvalidID(GenerationError):
def __init__(self, link):
super().__init__(link, "Link ID is invalid!", "invalidID")
class LinkNotSupported(GenerationError):
def __init__(self, link):
super().__init__(link, "Link is not supported.", "unsupportedURL")
class LinkNotRecognized(GenerationError):
def __init__(self, link):
super().__init__(link, "Link is not recognized.", "invalidURL")
class DownloadError(DeemixError):
"""Download related errors"""
ErrorMessages = {
'notOnDeezer': "Track not available on Deezer!",
'notEncoded': "Track not yet encoded!",
'notEncodedNoAlternative': "Track not yet encoded and no alternative found!",
'wrongBitrate': "Track not found at desired bitrate.",
'wrongBitrateNoAlternative': "Track not found at desired bitrate and no alternative found!",
'wrongLicense': "Your account can't stream the track at the desired bitrate.",
'no360RA': "Track is not available in Reality Audio 360.",
'notAvailable': "Track not available on deezer's servers!",
'notAvailableNoAlternative': "Track not available on deezer's servers and no alternative found!",
'noSpaceLeft': "No space left on target drive, clean up some space for the tracks",
'albumDoesntExists': "Track's album does not exsist, failed to gather info.",
'notLoggedIn': "You need to login to download tracks.",
'wrongGeolocation': "Your account can't stream the track from your current country.",
'wrongGeolocationNoAlternative': "Your account can't stream the track from your current country and no alternative found."
}
class DownloadFailed(DownloadError):
def __init__(self, errid, track=None):
super().__init__()
self.errid = errid
self.message = ErrorMessages[self.errid]
self.track = track
class PreferredBitrateNotFound(DownloadError):
pass
class TrackNot360(DownloadError):
pass
class DownloadCanceled(DownloadError):
pass
class DownloadEmpty(DownloadError):
pass
class TrackError(DeemixError):
"""Track generation related errors"""
class AlbumDoesntExists(TrackError):
pass
class MD5NotFound(TrackError):
pass
class NoDataToParse(TrackError):
pass

View File

@ -1,52 +1,47 @@
import logging import logging
from deezer.errors import GWAPIError, APIError
from deezer.utils import map_user_playlist, map_track, map_album
from deemix.types.DownloadObjects import Single, Collection from deemix.types.DownloadObjects import Single, Collection
from deemix.errors import GenerationError, ISRCnotOnDeezer, InvalidID, NotYourPrivatePlaylist from deezer.gw import GWAPIError, LyricsStatus
from deezer.api import APIError
from deezer.utils import map_user_playlist
logger = logging.getLogger('deemix') logger = logging.getLogger('deemix')
def generateTrackItem(dz, link_id, bitrate, trackAPI=None, albumAPI=None): def generateTrackItem(dz, link_id, bitrate, trackAPI=None, albumAPI=None):
# Get essential track info # Check if is an isrc: url
if not trackAPI: if str(link_id).startswith("isrc"):
if str(link_id).startswith("isrc") or int(link_id) > 0: try:
try: trackAPI = dz.api.get_track(link_id)
trackAPI = dz.api.get_track(link_id) except APIError as e:
except APIError as e: raise GenerationError(f"https://deezer.com/track/{link_id}", str(e)) from e
raise GenerationError(f"https://deezer.com/track/{link_id}", str(e)) from e
# Check if is an isrc: url if 'id' in trackAPI and 'title' in trackAPI:
if str(link_id).startswith("isrc"): link_id = trackAPI['id']
if 'id' in trackAPI and 'title' in trackAPI:
link_id = trackAPI['id']
else:
raise ISRCnotOnDeezer(f"https://deezer.com/track/{link_id}")
else: else:
trackAPI_gw = dz.gw.get_track(link_id) raise ISRCnotOnDeezer(f"https://deezer.com/track/{link_id}")
trackAPI = map_track(trackAPI_gw) if not link_id.isdecimal(): raise InvalidID(f"https://deezer.com/track/{link_id}")
else:
link_id = trackAPI['id']
if not str(link_id).strip('-').isdecimal(): raise InvalidID(f"https://deezer.com/track/{link_id}")
cover = None # Get essential track info
if trackAPI['album']['cover_small']: try:
cover = trackAPI['album']['cover_small'][:-24] + '/75x75-000000-80-0-0.jpg' trackAPI_gw = dz.gw.get_track_with_fallback(link_id)
else: except GWAPIError as e:
cover = f"https://e-cdns-images.dzcdn.net/images/cover/{trackAPI['md5_image']}/75x75-000000-80-0-0.jpg" raise GenerationError(f"https://deezer.com/track/{link_id}", str(e)) from e
if 'track_token' in trackAPI: del trackAPI['track_token'] title = trackAPI_gw['SNG_TITLE'].strip()
if trackAPI_gw.get('VERSION') and trackAPI_gw['VERSION'] not in trackAPI_gw['SNG_TITLE']:
title += f" {trackAPI_gw['VERSION']}".strip()
explicit = bool(int(trackAPI_gw.get('EXPLICIT_LYRICS', 0)))
return Single({ return Single({
'type': 'track', 'type': 'track',
'id': link_id, 'id': link_id,
'bitrate': bitrate, 'bitrate': bitrate,
'title': trackAPI['title'], 'title': title,
'artist': trackAPI['artist']['name'], 'artist': trackAPI_gw['ART_NAME'],
'cover': cover, 'cover': f"https://e-cdns-images.dzcdn.net/images/cover/{trackAPI_gw['ALB_PICTURE']}/75x75-000000-80-0-0.jpg",
'explicit': trackAPI['explicit_lyrics'], 'explicit': explicit,
'single': { 'single': {
'trackAPI_gw': trackAPI_gw,
'trackAPI': trackAPI, 'trackAPI': trackAPI,
'albumAPI': albumAPI 'albumAPI': albumAPI
} }
@ -69,25 +64,18 @@ def generateAlbumItem(dz, link_id, bitrate, rootArtist=None):
link_id = albumAPI['id'] link_id = albumAPI['id']
else: else:
try: try:
albumAPI_gw_page = dz.gw.get_album_page(link_id) albumAPI = dz.api.get_album(link_id)
if 'DATA' in albumAPI_gw_page:
albumAPI = map_album(albumAPI_gw_page['DATA'])
link_id = albumAPI_gw_page['DATA']['ALB_ID']
albumAPI_new = dz.api.get_album(link_id)
albumAPI.update(albumAPI_new)
else:
raise GenerationError(f"https://deezer.com/album/{link_id}", "Can't find the album")
except APIError as e: except APIError as e:
raise GenerationError(f"https://deezer.com/album/{link_id}", str(e)) from e raise GenerationError(f"https://deezer.com/album/{link_id}", str(e)) from e
if not str(link_id).isdecimal(): raise InvalidID(f"https://deezer.com/album/{link_id}") if not link_id.isdecimal(): raise InvalidID(f"https://deezer.com/album/{link_id}")
# Get extra info about album # Get extra info about album
# This saves extra api calls when downloading # This saves extra api calls when downloading
albumAPI_gw = dz.gw.get_album(link_id) albumAPI_gw = dz.gw.get_album(link_id)
albumAPI_gw = map_album(albumAPI_gw) albumAPI['nb_disk'] = albumAPI_gw['NUMBER_DISK']
albumAPI_gw.update(albumAPI) albumAPI['copyright'] = albumAPI_gw['COPYRIGHT']
albumAPI = albumAPI_gw albumAPI['release_date'] = albumAPI_gw['PHYSICAL_RELEASE_DATE']
albumAPI['root_artist'] = rootArtist albumAPI['root_artist'] = rootArtist
# If the album is a single download as a track # If the album is a single download as a track
@ -101,17 +89,18 @@ def generateAlbumItem(dz, link_id, bitrate, rootArtist=None):
if albumAPI['cover_small'] is not None: if albumAPI['cover_small'] is not None:
cover = albumAPI['cover_small'][:-24] + '/75x75-000000-80-0-0.jpg' cover = albumAPI['cover_small'][:-24] + '/75x75-000000-80-0-0.jpg'
else: else:
cover = f"https://e-cdns-images.dzcdn.net/images/cover/{albumAPI['md5_image']}/75x75-000000-80-0-0.jpg" cover = f"https://e-cdns-images.dzcdn.net/images/cover/{albumAPI_gw['ALB_PICTURE']}/75x75-000000-80-0-0.jpg"
totalSize = len(tracksArray) totalSize = len(tracksArray)
albumAPI['nb_tracks'] = totalSize albumAPI['nb_tracks'] = totalSize
collection = [] collection = []
for pos, trackAPI in enumerate(tracksArray, start=1): for pos, trackAPI in enumerate(tracksArray, start=1):
trackAPI = map_track(trackAPI) trackAPI['POSITION'] = pos
if 'track_token' in trackAPI: del trackAPI['track_token'] trackAPI['SIZE'] = totalSize
trackAPI['position'] = pos
collection.append(trackAPI) collection.append(trackAPI)
explicit = albumAPI_gw.get('EXPLICIT_ALBUM_CONTENT', {}).get('EXPLICIT_LYRICS_STATUS', LyricsStatus.UNKNOWN) in [LyricsStatus.EXPLICIT, LyricsStatus.PARTIALLY_EXPLICIT]
return Collection({ return Collection({
'type': 'album', 'type': 'album',
'id': link_id, 'id': link_id,
@ -119,17 +108,17 @@ def generateAlbumItem(dz, link_id, bitrate, rootArtist=None):
'title': albumAPI['title'], 'title': albumAPI['title'],
'artist': albumAPI['artist']['name'], 'artist': albumAPI['artist']['name'],
'cover': cover, 'cover': cover,
'explicit': albumAPI['explicit_lyrics'], 'explicit': explicit,
'size': totalSize, 'size': totalSize,
'collection': { 'collection': {
'tracks': collection, 'tracks_gw': collection,
'albumAPI': albumAPI 'albumAPI': albumAPI
} }
}) })
def generatePlaylistItem(dz, link_id, bitrate, playlistAPI=None, playlistTracksAPI=None): def generatePlaylistItem(dz, link_id, bitrate, playlistAPI=None, playlistTracksAPI=None):
if not playlistAPI: if not playlistAPI:
if not str(link_id).isdecimal(): raise InvalidID(f"https://deezer.com/playlist/{link_id}") if not link_id.isdecimal(): raise InvalidID(f"https://deezer.com/playlist/{link_id}")
# Get essential playlist info # Get essential playlist info
try: try:
playlistAPI = dz.api.get_playlist(link_id) playlistAPI = dz.api.get_playlist(link_id)
@ -156,11 +145,10 @@ def generatePlaylistItem(dz, link_id, bitrate, playlistAPI=None, playlistTracksA
playlistAPI['nb_tracks'] = totalSize playlistAPI['nb_tracks'] = totalSize
collection = [] collection = []
for pos, trackAPI in enumerate(playlistTracksAPI, start=1): for pos, trackAPI in enumerate(playlistTracksAPI, start=1):
trackAPI = map_track(trackAPI) if trackAPI.get('EXPLICIT_TRACK_CONTENT', {}).get('EXPLICIT_LYRICS_STATUS', LyricsStatus.UNKNOWN) in [LyricsStatus.EXPLICIT, LyricsStatus.PARTIALLY_EXPLICIT]:
if trackAPI['explicit_lyrics']:
playlistAPI['explicit'] = True playlistAPI['explicit'] = True
if 'track_token' in trackAPI: del trackAPI['track_token'] trackAPI['POSITION'] = pos
trackAPI['position'] = pos trackAPI['SIZE'] = totalSize
collection.append(trackAPI) collection.append(trackAPI)
if 'explicit' not in playlistAPI: playlistAPI['explicit'] = False if 'explicit' not in playlistAPI: playlistAPI['explicit'] = False
@ -175,13 +163,13 @@ def generatePlaylistItem(dz, link_id, bitrate, playlistAPI=None, playlistTracksA
'explicit': playlistAPI['explicit'], 'explicit': playlistAPI['explicit'],
'size': totalSize, 'size': totalSize,
'collection': { 'collection': {
'tracks': collection, 'tracks_gw': collection,
'playlistAPI': playlistAPI 'playlistAPI': playlistAPI
} }
}) })
def generateArtistItem(dz, link_id, bitrate, listener=None): def generateArtistItem(dz, link_id, bitrate, listener=None):
if not str(link_id).isdecimal(): raise InvalidID(f"https://deezer.com/artist/{link_id}") if not link_id.isdecimal(): raise InvalidID(f"https://deezer.com/artist/{link_id}")
# Get essential artist info # Get essential artist info
try: try:
artistAPI = dz.api.get_artist(link_id) artistAPI = dz.api.get_artist(link_id)
@ -208,7 +196,7 @@ def generateArtistItem(dz, link_id, bitrate, listener=None):
return albumList return albumList
def generateArtistDiscographyItem(dz, link_id, bitrate, listener=None): def generateArtistDiscographyItem(dz, link_id, bitrate, listener=None):
if not str(link_id).isdecimal(): raise InvalidID(f"https://deezer.com/artist/{link_id}/discography") if not link_id.isdecimal(): raise InvalidID(f"https://deezer.com/artist/{link_id}/discography")
# Get essential artist info # Get essential artist info
try: try:
artistAPI = dz.api.get_artist(link_id) artistAPI = dz.api.get_artist(link_id)
@ -236,7 +224,7 @@ def generateArtistDiscographyItem(dz, link_id, bitrate, listener=None):
return albumList return albumList
def generateArtistTopItem(dz, link_id, bitrate): def generateArtistTopItem(dz, link_id, bitrate):
if not str(link_id).isdecimal(): raise InvalidID(f"https://deezer.com/artist/{link_id}/top_track") if not link_id.isdecimal(): raise InvalidID(f"https://deezer.com/artist/{link_id}/top_track")
# Get essential artist info # Get essential artist info
try: try:
artistAPI = dz.api.get_artist(link_id) artistAPI = dz.api.get_artist(link_id)
@ -275,3 +263,45 @@ def generateArtistTopItem(dz, link_id, bitrate):
artistTopTracksAPI_gw = dz.gw.get_artist_toptracks(link_id) artistTopTracksAPI_gw = dz.gw.get_artist_toptracks(link_id)
return generatePlaylistItem(dz, playlistAPI['id'], bitrate, playlistAPI=playlistAPI, playlistTracksAPI=artistTopTracksAPI_gw) return generatePlaylistItem(dz, playlistAPI['id'], bitrate, playlistAPI=playlistAPI, playlistTracksAPI=artistTopTracksAPI_gw)
class GenerationError(Exception):
def __init__(self, link, message, errid=None):
super().__init__()
self.link = link
self.message = message
self.errid = errid
def toDict(self):
return {
'link': self.link,
'error': self.message,
'errid': self.errid
}
class ISRCnotOnDeezer(GenerationError):
def __init__(self, link):
super().__init__(link, "Track ISRC is not available on deezer", "ISRCnotOnDeezer")
class NotYourPrivatePlaylist(GenerationError):
def __init__(self, link):
super().__init__(link, "You can't download others private playlists.", "notYourPrivatePlaylist")
class TrackNotOnDeezer(GenerationError):
def __init__(self, link):
super().__init__(link, "Track not found on deezer!", "trackNotOnDeezer")
class AlbumNotOnDeezer(GenerationError):
def __init__(self, link):
super().__init__(link, "Album not found on deezer!", "albumNotOnDeezer")
class InvalidID(GenerationError):
def __init__(self, link):
super().__init__(link, "Link ID is invalid!", "invalidID")
class LinkNotSupported(GenerationError):
def __init__(self, link):
super().__init__(link, "Link is not supported.", "unsupportedURL")
class LinkNotRecognized(GenerationError):
def __init__(self, link):
super().__init__(link, "Link is not recognized.", "invalidURL")

View File

@ -1,19 +1,15 @@
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
import json import json
from copy import deepcopy
from pathlib import Path from pathlib import Path
import re import re
from urllib.request import urlopen from urllib.request import urlopen
from deezer.errors import DataException
from deemix.plugins import Plugin from deemix.plugins import Plugin
from deemix.utils.localpaths import getConfigFolder from deemix.utils.localpaths import getConfigFolder
from deemix.itemgen import generateTrackItem, generateAlbumItem from deemix.itemgen import generateTrackItem, generateAlbumItem, GenerationError, TrackNotOnDeezer, AlbumNotOnDeezer
from deemix.errors import GenerationError, TrackNotOnDeezer, AlbumNotOnDeezer from deemix.types.DownloadObjects import Convertable
from deemix.types.DownloadObjects import Convertable, Collection
import spotipy import spotipy
SpotifyClientCredentials = spotipy.oauth2.SpotifyClientCredentials SpotifyClientCredentials = spotipy.oauth2.SpotifyClientCredentials
CacheFileHandler = spotipy.cache_handler.CacheFileHandler
class Spotify(Plugin): class Spotify(Plugin):
def __init__(self, configFolder=None): def __init__(self, configFolder=None):
@ -58,7 +54,7 @@ class Spotify(Plugin):
return (link, link_type, link_id) return (link, link_type, link_id)
def generateDownloadObject(self, dz, link, bitrate, listener): def generateDownloadObject(self, dz, link, bitrate):
(link, link_type, link_id) = self.parseLink(link) (link, link_type, link_id) = self.parseLink(link)
if link_type is None or link_id is None: return None if link_type is None or link_id is None: return None
@ -95,10 +91,7 @@ class Spotify(Plugin):
cachedTrack['id'] = trackID cachedTrack['id'] = trackID
cache['tracks'][link_id] = cachedTrack cache['tracks'][link_id] = cachedTrack
self.saveCache(cache) self.saveCache(cache)
if cachedTrack['id'] != "0": return generateTrackItem(dz, cachedTrack['id'], bitrate)
if cachedTrack.get('id', "0") != "0":
return generateTrackItem(dz, cachedTrack['id'], bitrate)
raise TrackNotOnDeezer(f"https://open.spotify.com/track/{link_id}") raise TrackNotOnDeezer(f"https://open.spotify.com/track/{link_id}")
def generateAlbumItem(self, dz, link_id, bitrate): def generateAlbumItem(self, dz, link_id, bitrate):
@ -119,9 +112,9 @@ class Spotify(Plugin):
spotifyPlaylist = self.sp.playlist(link_id) spotifyPlaylist = self.sp.playlist(link_id)
playlistAPI = self._convertPlaylistStructure(spotifyPlaylist) playlistAPI = self._convertPlaylistStructure(spotifyPlaylist)
playlistAPI['various_artist'] = dz.api.get_artist(5080) # Useful for save as compilation playlistAPI.various_artist = dz.api.get_artist(5080) # Useful for save as compilation
tracklistTemp = spotifyPlaylist['tracks']['items'] tracklistTemp = spotifyPlaylist.track.items
while spotifyPlaylist['tracks']['next']: while spotifyPlaylist['tracks']['next']:
spotifyPlaylist['tracks'] = self.sp.next(spotifyPlaylist['tracks']) spotifyPlaylist['tracks'] = self.sp.next(spotifyPlaylist['tracks'])
tracklistTemp += spotifyPlaylist['tracks']['items'] tracklistTemp += spotifyPlaylist['tracks']['items']
@ -144,7 +137,7 @@ class Spotify(Plugin):
'explicit': playlistAPI['explicit'], 'explicit': playlistAPI['explicit'],
'size': len(tracklist), 'size': len(tracklist),
'collection': { 'collection': {
'tracks': [], 'tracks_gw': [],
'playlistAPI': playlistAPI 'playlistAPI': playlistAPI
}, },
'plugin': 'spotify', 'plugin': 'spotify',
@ -186,10 +179,8 @@ class Spotify(Plugin):
} }
return cachedAlbum return cachedAlbum
def convertTrack(self, dz, downloadObject, track, pos, conversion, cache, listener): def convertTrack(self, dz, downloadObject, track, pos, conversion, conversionNext, cache, listener):
if downloadObject.isCanceled: return if downloadObject.isCanceled: return
trackAPI = None
cachedTrack = None
if track['id'] in cache['tracks']: if track['id'] in cache['tracks']:
cachedTrack = cache['tracks'][track['id']] cachedTrack = cache['tracks'][track['id']]
@ -202,7 +193,7 @@ class Spotify(Plugin):
try: try:
trackAPI = dz.api.get_track_by_ISRC(cachedTrack['isrc']) trackAPI = dz.api.get_track_by_ISRC(cachedTrack['isrc'])
if 'id' not in trackAPI or 'title' not in trackAPI: trackAPI = None if 'id' not in trackAPI or 'title' not in trackAPI: trackAPI = None
except DataException: pass except GenerationError: pass
if self.settings['fallbackSearch'] and not trackAPI: if self.settings['fallbackSearch'] and not trackAPI:
if 'id' not in cachedTrack or cachedTrack['id'] == "0": if 'id' not in cachedTrack or cachedTrack['id'] == "0":
trackID = dz.api.get_track_id_from_metadata( trackID = dz.api.get_track_id_from_metadata(
@ -214,59 +205,46 @@ class Spotify(Plugin):
cachedTrack['id'] = trackID cachedTrack['id'] = trackID
cache['tracks'][track['id']] = cachedTrack cache['tracks'][track['id']] = cachedTrack
self.saveCache(cache) self.saveCache(cache)
if cachedTrack['id'] != "0": trackAPI = dz.api.get_track(cachedTrack['id'])
if cachedTrack.get('id', "0") != "0":
trackAPI = dz.api.get_track(cachedTrack['id'])
if not trackAPI: if not trackAPI:
trackAPI = { deezerTrack = {
'id': "0", 'SNG_ID': "0",
'title': track['name'], 'SNG_TITLE': track['name'],
'duration': 0, 'DURATION': 0,
'md5_origin': 0, 'MD5_ORIGIN': 0,
'media_version': 0, 'MEDIA_VERSION': 0,
'filesizes': {}, 'FILESIZE': 0,
'album': { 'ALB_TITLE': track['album']['name'],
'title': track['album']['name'], 'ALB_PICTURE': "",
'md5_image': "" 'ART_ID': 0,
}, 'ART_NAME': track['artists'][0]['name']
'artist': {
'id': 0,
'name': track['artists'][0]['name']
}
} }
trackAPI['position'] = pos+1 else:
deezerTrack = dz.gw.get_track_with_fallback(trackAPI['id'])
deezerTrack['_EXTRA_TRACK'] = trackAPI
deezerTrack['POSITION'] = pos+1
conversion['next'] += (1 / downloadObject.size) * 100 conversionNext += (1 / downloadObject.size) * 100
if round(conversion['next']) != conversion['now'] and round(conversion['next']) % 2 == 0: if round(conversionNext) != conversion and round(conversionNext) % 2 == 0:
conversion['now'] = round(conversion['next']) conversion = round(conversionNext)
if listener: listener.send("updateQueue", {'uuid': downloadObject.uuid, 'conversion': conversion['now']}) if listener: listener.send("updateQueue", {'uuid': downloadObject.uuid, 'conversion': conversion})
return trackAPI
def convert(self, dz, downloadObject, settings, listener=None): def convert(self, dz, downloadObject, settings, listener=None):
cache = self.loadCache() cache = self.loadCache()
conversion = { 'now': 0, 'next': 0 } conversion = 0
conversionNext = 0
collection = [None] * len(downloadObject.conversion_data) collection = [None] * len(downloadObject.conversion_data)
if listener: listener.send("startConversion", downloadObject.uuid)
with ThreadPoolExecutor(settings['queueConcurrency']) as executor: with ThreadPoolExecutor(settings['queueConcurrency']) as executor:
for pos, track in enumerate(downloadObject.conversion_data, start=0): for pos, track in enumerate(downloadObject.conversion_data, start=0):
collection[pos] = executor.submit(self.convertTrack, collection[pos] = executor.submit(self.convertTrack,
dz, downloadObject, dz, downloadObject,
track, pos, track, pos,
conversion, conversion, conversionNext,
cache, listener cache, listener
).result() )
downloadObject.collection['tracks'] = collection
downloadObject.size = len(collection)
downloadObject = Collection(downloadObject.toDict())
if listener: listener.send("finishConversion", downloadObject.getSlimmedDict())
self.saveCache(cache)
return downloadObject
@classmethod @classmethod
def _convertPlaylistStructure(cls, spotifyPlaylist): def _convertPlaylistStructure(cls, spotifyPlaylist):
@ -295,7 +273,6 @@ class Spotify(Plugin):
'picture_medium': cover or "https://e-cdns-images.dzcdn.net/images/cover/d41d8cd98f00b204e9800998ecf8427e/250x250-000000-80-0-0.jpg", 'picture_medium': cover or "https://e-cdns-images.dzcdn.net/images/cover/d41d8cd98f00b204e9800998ecf8427e/250x250-000000-80-0-0.jpg",
'picture_big': cover or "https://e-cdns-images.dzcdn.net/images/cover/d41d8cd98f00b204e9800998ecf8427e/500x500-000000-80-0-0.jpg", 'picture_big': cover or "https://e-cdns-images.dzcdn.net/images/cover/d41d8cd98f00b204e9800998ecf8427e/500x500-000000-80-0-0.jpg",
'picture_xl': cover or "https://e-cdns-images.dzcdn.net/images/cover/d41d8cd98f00b204e9800998ecf8427e/1000x1000-000000-80-0-0.jpg", 'picture_xl': cover or "https://e-cdns-images.dzcdn.net/images/cover/d41d8cd98f00b204e9800998ecf8427e/1000x1000-000000-80-0-0.jpg",
'picture_thumbnail': cover or "https://e-cdns-images.dzcdn.net/images/cover/d41d8cd98f00b204e9800998ecf8427e/75x75-000000-80-0-0.jpg",
'public': spotifyPlaylist['public'], 'public': spotifyPlaylist['public'],
'share': spotifyPlaylist['external_urls']['spotify'], 'share': spotifyPlaylist['external_urls']['spotify'],
'title': spotifyPlaylist['name'], 'title': spotifyPlaylist['name'],
@ -305,27 +282,19 @@ class Spotify(Plugin):
return deezerPlaylist return deezerPlaylist
def loadSettings(self): def loadSettings(self):
if not (self.configFolder / 'config.json').is_file(): if not (self.configFolder / 'settings.json').is_file():
with open(self.configFolder / 'config.json', 'w', encoding="utf-8") as f: with open(self.configFolder / 'settings.json', 'w') as f:
json.dump({**self.credentials, **self.settings}, f, indent=2) json.dump({**self.credentials, **self.settings}, f, indent=2)
with open(self.configFolder / 'config.json', 'r', encoding="utf-8") as settingsFile: with open(self.configFolder / 'settings.json', 'r') as settingsFile:
try: settings = json.load(settingsFile)
settings = json.load(settingsFile)
except json.decoder.JSONDecodeError:
with open(self.configFolder / 'config.json', 'w', encoding="utf-8") as f:
json.dump({**self.credentials, **self.settings}, f, indent=2)
settings = deepcopy({**self.credentials, **self.settings})
except Exception:
settings = deepcopy({**self.credentials, **self.settings})
self.setSettings(settings) self.setSettings(settings)
self.checkCredentials() self.checkCredentials()
def saveSettings(self, newSettings=None): def saveSettings(self, newSettings=None):
if newSettings: self.setSettings(newSettings) if newSettings: self.setSettings(newSettings)
self.checkCredentials() self.checkCredentials()
with open(self.configFolder / 'config.json', 'w', encoding="utf-8") as f: with open(self.configFolder / 'settings.json', 'w') as f:
json.dump({**self.credentials, **self.settings}, f, indent=2) json.dump({**self.credentials, **self.settings}, f, indent=2)
def getSettings(self): def getSettings(self):
@ -339,21 +308,15 @@ class Spotify(Plugin):
self.settings = settings self.settings = settings
def loadCache(self): def loadCache(self):
cache = None
if (self.configFolder / 'cache.json').is_file(): if (self.configFolder / 'cache.json').is_file():
with open(self.configFolder / 'cache.json', 'r', encoding="utf-8") as f: with open(self.configFolder / 'cache.json', 'r') as f:
try: cache = json.load(f)
cache = json.load(f) else:
except json.decoder.JSONDecodeError: cache = {'tracks': {}, 'albums': {}}
self.saveCache({'tracks': {}, 'albums': {}})
cache = None
except Exception:
cache = None
if not cache: cache = {'tracks': {}, 'albums': {}}
return cache return cache
def saveCache(self, newCache): def saveCache(self, newCache):
with open(self.configFolder / 'cache.json', 'w', encoding="utf-8") as spotifyCache: with open(self.configFolder / 'cache.json', 'w') as spotifyCache:
json.dump(newCache, spotifyCache) json.dump(newCache, spotifyCache)
def checkCredentials(self): def checkCredentials(self):
@ -362,10 +325,8 @@ class Spotify(Plugin):
return return
try: try:
cache_handler = CacheFileHandler(self.configFolder / ".auth-cache")
client_credentials_manager = SpotifyClientCredentials(client_id=self.credentials['clientId'], client_credentials_manager = SpotifyClientCredentials(client_id=self.credentials['clientId'],
client_secret=self.credentials['clientSecret'], client_secret=self.credentials['clientSecret'])
cache_handler=cache_handler)
self.sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) self.sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
self.sp.user_playlists('spotify') self.sp.user_playlists('spotify')
self.enabled = True self.enabled = True

View File

@ -1,5 +1,4 @@
import json import json
from copy import deepcopy
from pathlib import Path from pathlib import Path
from os import makedirs from os import makedirs
from deezer import TrackFormats from deezer import TrackFormats
@ -21,93 +20,89 @@ class FeaturesOption():
MOVE_TITLE = "2" # Move to track title MOVE_TITLE = "2" # Move to track title
DEFAULTS = { DEFAULTS = {
"albumNameTemplate": "%artist% - %album%",
"albumTracknameTemplate": "%tracknumber% - %title%",
"albumVariousArtists": True,
"artistCasing": "nothing",
"artistImageTemplate": "folder",
"artistNameTemplate": "%artist%",
"coverImageTemplate": "cover",
"createAlbumFolder": True,
"createArtistFolder": False,
"createCDFolder": True,
"createM3U8File": False,
"createPlaylistFolder": True,
"createSingleFolder": False,
"createStructurePlaylist": False,
"dateFormat": "Y-M-D",
"downloadLocation": str(localpaths.getMusicFolder()), "downloadLocation": str(localpaths.getMusicFolder()),
"embeddedArtworkPNG": False, "tracknameTemplate": "%artist% - %title%",
"embeddedArtworkSize": 800, "albumTracknameTemplate": "%tracknumber% - %title%",
"executeCommand": "", "playlistTracknameTemplate": "%position% - %artist% - %title%",
"fallbackBitrate": False, "createPlaylistFolder": True,
"fallbackISRC": False, "playlistNameTemplate": "%playlist%",
"fallbackSearch": False, "createArtistFolder": False,
"featuredToTitle": FeaturesOption.NO_CHANGE, "artistNameTemplate": "%artist%",
"feelingLucky": False, "createAlbumFolder": True,
"albumNameTemplate": "%artist% - %album%",
"createCDFolder": True,
"createStructurePlaylist": False,
"createSingleFolder": False,
"padTracks": True,
"paddingSize": "0",
"illegalCharacterReplacer": "_", "illegalCharacterReplacer": "_",
"jpegImageQuality": 90, "queueConcurrency": 3,
"localArtworkFormat": "jpg", "maxBitrate": str(TrackFormats.MP3_320),
"localArtworkSize": 1400, "fallbackBitrate": True,
"fallbackSearch": False,
"logErrors": True, "logErrors": True,
"logSearched": False, "logSearched": False,
"maxBitrate": TrackFormats.MP3_320,
"overwriteFile": OverwriteOption.DONT_OVERWRITE, "overwriteFile": OverwriteOption.DONT_OVERWRITE,
"paddingSize": "0", "createM3U8File": False,
"padTracks": True,
"playlistFilenameTemplate": "playlist", "playlistFilenameTemplate": "playlist",
"playlistNameTemplate": "%playlist%",
"playlistTracknameTemplate": "%position% - %artist% - %title%",
"queueConcurrency": 3,
"removeAlbumVersion": False,
"removeDuplicateArtists": True,
"saveArtwork": True,
"saveArtworkArtist": False,
"syncedLyrics": False, "syncedLyrics": False,
"embeddedArtworkSize": 800,
"embeddedArtworkPNG": False,
"localArtworkSize": 1400,
"localArtworkFormat": "jpg",
"saveArtwork": True,
"coverImageTemplate": "cover",
"saveArtworkArtist": False,
"artistImageTemplate": "folder",
"jpegImageQuality": 80,
"dateFormat": "Y-M-D",
"albumVariousArtists": True,
"removeAlbumVersion": False,
"removeDuplicateArtists": False,
"featuredToTitle": FeaturesOption.NO_CHANGE,
"titleCasing": "nothing",
"artistCasing": "nothing",
"executeCommand": "",
"tags": { "tags": {
"album": True,
"albumArtist": True,
"artist": True,
"artists": True,
"barcode": True,
"bpm": True,
"composer": False,
"copyright": False,
"cover": True,
"coverDescriptionUTF8": False,
"date": True,
"discNumber": True,
"discTotal": False,
"explicit": False,
"genre": True,
"involvedPeople": False,
"isrc": True,
"label": True,
"length": True,
"lyrics": False,
"multiArtistSeparator": "default",
"rating": False,
"replayGain": False,
"saveID3v1": True,
"savePlaylistAsCompilation": False,
"singleAlbumArtist": False,
"source": False,
"syncedLyrics": False,
"title": True, "title": True,
"artist": True,
"album": True,
"cover": True,
"trackNumber": True, "trackNumber": True,
"trackTotal": False, "trackTotal": False,
"useNullSeparator": False, "discNumber": True,
"discTotal": False,
"albumArtist": True,
"genre": True,
"year": True, "year": True,
}, "date": True,
"titleCasing": "nothing", "explicit": False,
"tracknameTemplate": "%artist% - %title%", "isrc": True,
"length": True,
"barcode": True,
"bpm": True,
"replayGain": False,
"label": True,
"lyrics": False,
"syncedLyrics": False,
"copyright": False,
"composer": False,
"involvedPeople": False,
"source": False,
"savePlaylistAsCompilation": False,
"useNullSeparator": False,
"saveID3v1": True,
"multiArtistSeparator": "default",
"singleAlbumArtist": False,
"coverDescriptionUTF8": False
}
} }
def save(settings, configFolder=None): def save(settings, configFolder=None):
configFolder = Path(configFolder or localpaths.getConfigFolder()) configFolder = Path(configFolder or localpaths.getConfigFolder())
makedirs(configFolder, exist_ok=True) # Create config folder if it doesn't exsist makedirs(configFolder, exist_ok=True) # Create config folder if it doesn't exsist
with open(configFolder / 'config.json', 'w', encoding="utf-8") as configFile: with open(configFolder / 'config.json', 'w') as configFile:
json.dump(settings, configFile, indent=2) json.dump(settings, configFile, indent=2)
def load(configFolder=None): def load(configFolder=None):
@ -116,26 +111,16 @@ def load(configFolder=None):
if not (configFolder / 'config.json').is_file(): save(DEFAULTS, configFolder) # Create config file if it doesn't exsist if not (configFolder / 'config.json').is_file(): save(DEFAULTS, configFolder) # Create config file if it doesn't exsist
# Read config file # Read config file
with open(configFolder / 'config.json', 'r', encoding="utf-8") as configFile: with open(configFolder / 'config.json', 'r') as configFile:
try: settings = json.load(configFile)
settings = json.load(configFile)
except json.decoder.JSONDecodeError:
save(DEFAULTS, configFolder)
settings = deepcopy(DEFAULTS)
except Exception:
settings = deepcopy(DEFAULTS)
if check(settings) > 0: if check(settings) > 0: save(settings, configFolder) # Check the settings and save them if something changed
try:
save(settings, configFolder) # Check the settings and save them if something changed
except:
print(f"Error saving config file {configFile.name}, continuing without saving.")
return settings return settings
def check(settings): def check(settings):
changes = 0 changes = 0
for i_set in DEFAULTS: for i_set in DEFAULTS:
if not i_set in settings or not type(settings[i_set] is type(DEFAULTS[i_set])): if not i_set in settings or not isinstance(settings[i_set], type(DEFAULTS[i_set])):
settings[i_set] = DEFAULTS[i_set] settings[i_set] = DEFAULTS[i_set]
changes += 1 changes += 1
for i_set in DEFAULTS['tags']: for i_set in DEFAULTS['tags']:

View File

@ -1,7 +1,7 @@
from mutagen.flac import FLAC, Picture from mutagen.flac import FLAC, Picture
from mutagen.id3 import ID3, ID3NoHeaderError, \ from mutagen.id3 import ID3, ID3NoHeaderError, \
TXXX, TIT2, TPE1, TALB, TPE2, TRCK, TPOS, TCON, TYER, TDAT, TLEN, TBPM, \ TXXX, TIT2, TPE1, TALB, TPE2, TRCK, TPOS, TCON, TYER, TDAT, TLEN, TBPM, \
TPUB, TSRC, USLT, SYLT, APIC, IPLS, TCOM, TCOP, TCMP, Encoding, PictureType, POPM TPUB, TSRC, USLT, SYLT, APIC, IPLS, TCOM, TCOP, TCMP, Encoding, PictureType
# Adds tags to a MP3 file # Adds tags to a MP3 file
def tagID3(path, track, save): def tagID3(path, track, save):
@ -25,8 +25,7 @@ def tagID3(path, track, save):
tag.add(TPE1(text=track.artistsString)) tag.add(TPE1(text=track.artistsString))
# Tag ARTISTS is added to keep the multiartist support when using a non standard tagging method # Tag ARTISTS is added to keep the multiartist support when using a non standard tagging method
# https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html#artists # https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html#artists
if save['artists']: tag.add(TXXX(desc="ARTISTS", text=track.artists))
tag.add(TXXX(desc="ARTISTS", text=track.artists))
if save['album']: if save['album']:
tag.add(TALB(text=track.album.title)) tag.add(TALB(text=track.album.title))
@ -59,7 +58,7 @@ def tagID3(path, track, save):
tag.add(TDAT(text=str(track.date.day) + str(track.date.month))) tag.add(TDAT(text=str(track.date.day) + str(track.date.month)))
if save['length']: if save['length']:
tag.add(TLEN(text=str(int(track.duration)*1000))) tag.add(TLEN(text=str(int(track.duration)*1000)))
if save['bpm'] and track.bpm: if save['bpm']:
tag.add(TBPM(text=str(track.bpm))) tag.add(TBPM(text=str(track.bpm)))
if save['label']: if save['label']:
tag.add(TPUB(text=track.album.label)) tag.add(TPUB(text=track.album.label))
@ -90,7 +89,7 @@ def tagID3(path, track, save):
if len(involved_people) > 0 and save['involvedPeople']: if len(involved_people) > 0 and save['involvedPeople']:
tag.add(IPLS(people=involved_people)) tag.add(IPLS(people=involved_people))
if save['copyright'] and track.copyright: if save['copyright']:
tag.add(TCOP(text=track.copyright)) tag.add(TCOP(text=track.copyright))
if save['savePlaylistAsCompilation'] and track.playlist or track.album.recordType == "compile": if save['savePlaylistAsCompilation'] and track.playlist or track.album.recordType == "compile":
tag.add(TCMP(text="1")) tag.add(TCMP(text="1"))
@ -99,15 +98,6 @@ def tagID3(path, track, save):
tag.add(TXXX(desc="SOURCE", text='Deezer')) tag.add(TXXX(desc="SOURCE", text='Deezer'))
tag.add(TXXX(desc="SOURCEID", text=str(track.id))) tag.add(TXXX(desc="SOURCEID", text=str(track.id)))
if save['rating']:
rank = round((int(track.rank) / 10000) * 2.55)
if rank > 255 :
rank = 255
else:
rank = round(rank, 0)
tag.add(POPM(rating=rank))
if save['cover'] and track.album.embeddedCoverPath: if save['cover'] and track.album.embeddedCoverPath:
descEncoding = Encoding.LATIN1 descEncoding = Encoding.LATIN1
@ -146,8 +136,7 @@ def tagFLAC(path, track, save):
tag["ARTIST"] = track.artistsString tag["ARTIST"] = track.artistsString
# Tag ARTISTS is added to keep the multiartist support when using a non standard tagging method # Tag ARTISTS is added to keep the multiartist support when using a non standard tagging method
# https://picard-docs.musicbrainz.org/en/technical/tag_mapping.html#artists # https://picard-docs.musicbrainz.org/en/technical/tag_mapping.html#artists
if save['artists']: tag["ARTISTS"] = track.artists
tag["ARTISTS"] = track.artists
if save['album']: if save['album']:
tag["ALBUM"] = track.album.title tag["ALBUM"] = track.album.title
@ -179,7 +168,7 @@ def tagFLAC(path, track, save):
if save['length']: if save['length']:
tag["LENGTH"] = str(int(track.duration)*1000) tag["LENGTH"] = str(int(track.duration)*1000)
if save['bpm'] and track.bpm: if save['bpm']:
tag["BPM"] = str(track.bpm) tag["BPM"] = str(track.bpm)
if save['label']: if save['label']:
tag["PUBLISHER"] = track.album.label tag["PUBLISHER"] = track.album.label
@ -201,7 +190,7 @@ def tagFLAC(path, track, save):
elif role == 'musicpublisher' and save['involvedPeople']: elif role == 'musicpublisher' and save['involvedPeople']:
tag["ORGANIZATION"] = track.contributors['musicpublisher'] tag["ORGANIZATION"] = track.contributors['musicpublisher']
if save['copyright'] and track.copyright: if save['copyright']:
tag["COPYRIGHT"] = track.copyright tag["COPYRIGHT"] = track.copyright
if save['savePlaylistAsCompilation'] and track.playlist or track.album.recordType == "compile": if save['savePlaylistAsCompilation'] and track.playlist or track.album.recordType == "compile":
tag["COMPILATION"] = "1" tag["COMPILATION"] = "1"
@ -210,10 +199,6 @@ def tagFLAC(path, track, save):
tag["SOURCE"] = 'Deezer' tag["SOURCE"] = 'Deezer'
tag["SOURCEID"] = str(track.id) tag["SOURCEID"] = str(track.id)
if save['rating']:
rank = round((int(track.rank) / 10000))
tag['RATING'] = str(rank)
if save['cover'] and track.album.embeddedCoverPath: if save['cover'] and track.album.embeddedCoverPath:
image = Picture() image = Picture()
image.type = PictureType.COVER_FRONT image.type = PictureType.COVER_FRONT

View File

@ -1,3 +1,5 @@
from deezer.gw import LyricsStatus
from deemix.utils import removeDuplicateArtists, removeFeatures from deemix.utils import removeDuplicateArtists, removeFeatures
from deemix.types.Artist import Artist from deemix.types.Artist import Artist
from deemix.types.Date import Date from deemix.types.Date import Date
@ -28,7 +30,7 @@ class Album:
self.rootArtist = None self.rootArtist = None
self.variousArtists = None self.variousArtists = None
self.playlistID = None self.playlistId = None
self.owner = None self.owner = None
self.isPlaylist = False self.isPlaylist = False
@ -37,9 +39,8 @@ class Album:
# Getting artist image ID # Getting artist image ID
# ex: https://e-cdns-images.dzcdn.net/images/artist/f2bc007e9133c946ac3c3907ddc5d2ea/56x56-000000-80-0-0.jpg # ex: https://e-cdns-images.dzcdn.net/images/artist/f2bc007e9133c946ac3c3907ddc5d2ea/56x56-000000-80-0-0.jpg
art_pic = albumAPI['artist'].get('picture_small') art_pic = albumAPI['artist']['picture_small']
if art_pic: art_pic = art_pic[art_pic.find('artist/') + 7:-24] art_pic = art_pic[art_pic.find('artist/') + 7:-24]
else: art_pic = ""
self.mainArtist = Artist( self.mainArtist = Artist(
albumAPI['artist']['id'], albumAPI['artist']['id'],
albumAPI['artist']['name'], albumAPI['artist']['name'],
@ -77,36 +78,57 @@ class Album:
self.artist[artist['role']].append(artist['name']) self.artist[artist['role']].append(artist['name'])
self.trackTotal = albumAPI['nb_tracks'] self.trackTotal = albumAPI['nb_tracks']
self.recordType = albumAPI.get('record_type', self.recordType) self.recordType = albumAPI['record_type']
self.barcode = albumAPI.get('upc', self.barcode) self.barcode = albumAPI.get('upc', self.barcode)
self.label = albumAPI.get('label', self.label) self.label = albumAPI.get('label', self.label)
self.explicit = bool(albumAPI.get('explicit_lyrics', False)) self.explicit = bool(albumAPI.get('explicit_lyrics', False))
release_date = albumAPI.get('release_date') if 'release_date' in albumAPI:
if 'physical_release_date' in albumAPI: self.date.day = albumAPI["release_date"][8:10]
release_date = albumAPI['physical_release_date'] self.date.month = albumAPI["release_date"][5:7]
if release_date: self.date.year = albumAPI["release_date"][0:4]
self.date.day = release_date[8:10]
self.date.month = release_date[5:7]
self.date.year = release_date[0:4]
self.date.fixDayMonth() self.date.fixDayMonth()
self.discTotal = albumAPI.get('nb_disk', "1") self.discTotal = albumAPI.get('nb_disk')
self.copyright = albumAPI.get('copyright', "") self.copyright = albumAPI.get('copyright')
if not self.pic.md5 or self.pic.md5 == "": if self.pic.md5 == "":
if albumAPI.get('md5_image'): # Getting album cover MD5
self.pic.md5 = albumAPI['md5_image'] # ex: https://e-cdns-images.dzcdn.net/images/cover/2e018122cb56986277102d2041a592c8/56x56-000000-80-0-0.jpg
elif albumAPI.get('cover_small'): alb_pic = albumAPI['cover_small']
# Getting album cover MD5 self.pic.md5 = alb_pic[alb_pic.find('cover/') + 6:-24]
# ex: https://e-cdns-images.dzcdn.net/images/cover/2e018122cb56986277102d2041a592c8/56x56-000000-80-0-0.jpg
alb_pic = albumAPI['cover_small']
self.pic.md5 = alb_pic[alb_pic.find('cover/') + 6:-24]
if albumAPI.get('genres') and len(albumAPI['genres'].get('data', [])) > 0: if albumAPI.get('genres') and len(albumAPI['genres'].get('data', [])) > 0:
for genre in albumAPI['genres']['data']: for genre in albumAPI['genres']['data']:
self.genre.append(genre['name']) self.genre.append(genre['name'])
def parseAlbumGW(self, albumAPI_gw):
self.title = albumAPI_gw['ALB_TITLE']
self.mainArtist = Artist(
art_id = albumAPI_gw['ART_ID'],
name = albumAPI_gw['ART_NAME'],
role = "Main"
)
self.artists = [albumAPI_gw['ART_NAME']]
self.trackTotal = albumAPI_gw['NUMBER_TRACK']
self.discTotal = albumAPI_gw['NUMBER_DISK']
self.label = albumAPI_gw.get('LABEL_NAME', self.label)
explicitLyricsStatus = albumAPI_gw.get('EXPLICIT_ALBUM_CONTENT', {}).get('EXPLICIT_LYRICS_STATUS', LyricsStatus.UNKNOWN)
self.explicit = explicitLyricsStatus in [LyricsStatus.EXPLICIT, LyricsStatus.PARTIALLY_EXPLICIT]
self.addExtraAlbumGWData(albumAPI_gw)
def addExtraAlbumGWData(self, albumAPI_gw):
if self.pic.md5 == "":
self.pic.md5 = albumAPI_gw['ALB_PICTURE']
if 'PHYSICAL_RELEASE_DATE' in albumAPI_gw:
self.date.day = albumAPI_gw["PHYSICAL_RELEASE_DATE"][8:10]
self.date.month = albumAPI_gw["PHYSICAL_RELEASE_DATE"][5:7]
self.date.year = albumAPI_gw["PHYSICAL_RELEASE_DATE"][0:4]
self.date.fixDayMonth()
def makePlaylistCompilation(self, playlist): def makePlaylistCompilation(self, playlist):
self.variousArtists = playlist.variousArtists self.variousArtists = playlist.variousArtists
self.mainArtist = playlist.mainArtist self.mainArtist = playlist.mainArtist
@ -121,7 +143,7 @@ class Album:
self.explicit = playlist.explicit self.explicit = playlist.explicit
self.date = playlist.date self.date = playlist.date
self.discTotal = playlist.discTotal self.discTotal = playlist.discTotal
self.playlistID = playlist.playlistID self.playlistId = playlist.playlistId
self.owner = playlist.owner self.owner = playlist.owner
self.pic = playlist.pic self.pic = playlist.pic
self.isPlaylist = True self.isPlaylist = True

View File

@ -1,5 +1,3 @@
from pathlib import Path
class IDownloadObject: class IDownloadObject:
"""DownloadObject Interface""" """DownloadObject Interface"""
def __init__(self, obj): def __init__(self, obj):
@ -16,8 +14,6 @@ class IDownloadObject:
self.progress = obj.get('progress', 0) self.progress = obj.get('progress', 0)
self.errors = obj.get('errors', []) self.errors = obj.get('errors', [])
self.files = obj.get('files', []) self.files = obj.get('files', [])
self.extrasPath = obj.get('extrasPath', "")
if self.extrasPath: self.extrasPath = Path(self.extrasPath)
self.progressNext = 0 self.progressNext = 0
self.uuid = f"{self.type}_{self.id}_{self.bitrate}" self.uuid = f"{self.type}_{self.id}_{self.bitrate}"
self.isCanceled = False self.isCanceled = False
@ -39,7 +35,6 @@ class IDownloadObject:
'progress': self.progress, 'progress': self.progress,
'errors': self.errors, 'errors': self.errors,
'files': self.files, 'files': self.files,
'extrasPath': str(self.extrasPath),
'__type__': self.__type__ '__type__': self.__type__
} }
@ -70,8 +65,7 @@ class IDownloadObject:
'artist': self.artist, 'artist': self.artist,
'cover': self.cover, 'cover': self.cover,
'explicit': self.explicit, 'explicit': self.explicit,
'size': self.size, 'size': self.size
'extrasPath': str(self.extrasPath)
} }
def updateProgress(self, listener=None): def updateProgress(self, listener=None):

View File

@ -25,5 +25,5 @@ class StaticPicture:
def __init__(self, url): def __init__(self, url):
self.staticURL = url self.staticURL = url
def getURL(self, _, __): def getURL(self):
return self.staticURL return self.staticURL

View File

@ -1,9 +1,9 @@
from time import sleep
import re import re
from datetime import datetime import requests
from deezer.utils import map_track, map_album from deezer.gw import GWAPIError
from deezer.errors import APIError, GWAPIError from deezer.api import APIError
from deemix.errors import NoDataToParse, AlbumDoesntExists
from deemix.utils import removeFeatures, andCommaConcat, removeDuplicateArtists, generateReplayGainString, changeCase from deemix.utils import removeFeatures, andCommaConcat, removeDuplicateArtists, generateReplayGainString, changeCase
@ -23,11 +23,8 @@ class Track:
self.title = name self.title = name
self.MD5 = "" self.MD5 = ""
self.mediaVersion = "" self.mediaVersion = ""
self.trackToken = ""
self.trackTokenExpiration = 0
self.duration = 0 self.duration = 0
self.fallbackID = "0" self.fallbackID = "0"
self.albumsFallback = []
self.filesizes = {} self.filesizes = {}
self.local = False self.local = False
self.mainArtist = None self.mainArtist = None
@ -44,7 +41,6 @@ class Track:
self.explicit = False self.explicit = False
self.ISRC = "" self.ISRC = ""
self.replayGain = "" self.replayGain = ""
self.rank = 0
self.playlist = None self.playlist = None
self.position = None self.position = None
self.searched = False self.searched = False
@ -54,57 +50,78 @@ class Track:
self.artistsString = "" self.artistsString = ""
self.mainArtistsString = "" self.mainArtistsString = ""
self.featArtistsString = "" self.featArtistsString = ""
self.urls = {}
def parseEssentialData(self, trackAPI): def parseEssentialData(self, trackAPI_gw, trackAPI=None):
self.id = str(trackAPI['id']) self.id = str(trackAPI_gw['SNG_ID'])
self.duration = trackAPI['duration'] self.duration = trackAPI_gw['DURATION']
self.trackToken = trackAPI['track_token'] self.MD5 = trackAPI_gw.get('MD5_ORIGIN')
self.trackTokenExpiration = trackAPI['track_token_expire'] if not self.MD5:
self.MD5 = trackAPI.get('md5_origin') if trackAPI and trackAPI.get('md5_origin'):
self.mediaVersion = trackAPI['media_version'] self.MD5 = trackAPI['md5_origin']
self.filesizes = trackAPI['filesizes'] else:
raise MD5NotFound
self.mediaVersion = trackAPI_gw['MEDIA_VERSION']
self.fallbackID = "0" self.fallbackID = "0"
if 'fallback_id' in trackAPI: if 'FALLBACK' in trackAPI_gw:
self.fallbackID = trackAPI['fallback_id'] self.fallbackID = trackAPI_gw['FALLBACK']['SNG_ID']
self.local = int(self.id) < 0 self.local = int(self.id) < 0
self.urls = {}
def parseData(self, dz, track_id=None, trackAPI=None, albumAPI=None, playlistAPI=None): def retriveFilesizes(self, dz):
if track_id and (not trackAPI or trackAPI and not trackAPI.get('track_token')): guest_sid = dz.session.cookies.get('sid')
trackAPI_new = dz.gw.get_track_with_fallback(track_id) try:
trackAPI_new = map_track(trackAPI_new) site = requests.post(
if not trackAPI: trackAPI = {} "https://api.deezer.com/1.0/gateway.php",
trackAPI_new.update(trackAPI) params={
trackAPI = trackAPI_new 'api_key': "4VCYIJUCDLOUELGD1V8WBVYBNVDYOXEWSLLZDONGBBDFVXTZJRXPR29JRLQFO6ZE",
elif not trackAPI: raise NoDataToParse 'sid': guest_sid,
'input': '3',
'output': '3',
'method': 'song_getData'
},
timeout=30,
json={'sng_id': self.id},
headers=dz.http_headers
)
result_json = site.json()
except:
sleep(2)
self.retriveFilesizes(dz)
if len(result_json['error']):
raise TrackError(result_json.dumps(result_json['error']))
response = result_json.get("results", {})
filesizes = {}
for key, value in response.items():
if key.startswith("FILESIZE_"):
filesizes[key] = int(value)
filesizes[key+"_TESTED"] = False
self.filesizes = filesizes
self.parseEssentialData(trackAPI) def parseData(self, dz, track_id=None, trackAPI_gw=None, trackAPI=None, albumAPI_gw=None, albumAPI=None, playlistAPI=None):
if track_id and not trackAPI_gw: trackAPI_gw = dz.gw.get_track_with_fallback(track_id)
elif not trackAPI_gw: raise NoDataToParse
if not trackAPI:
try: trackAPI = dz.api.get_track(trackAPI_gw['SNG_ID'])
except APIError: trackAPI = None
# only public api has bpm self.parseEssentialData(trackAPI_gw, trackAPI)
if not trackAPI.get('bpm') and not self.local:
try:
trackAPI_new = dz.api.get_track(trackAPI['id'])
trackAPI_new['release_date'] = trackAPI['release_date']
trackAPI.update(trackAPI_new)
except APIError: pass
if self.local: if self.local:
self.parseLocalTrackData(trackAPI) self.parseLocalTrackData(trackAPI_gw)
else: else:
self.parseTrack(trackAPI) self.retriveFilesizes(dz)
self.parseTrackGW(trackAPI_gw)
# Get Lyrics data # Get Lyrics data
if not trackAPI.get("lyrics") and self.lyrics.id != "0": if not "LYRICS" in trackAPI_gw and self.lyrics.id != "0":
try: trackAPI["lyrics"] = dz.gw.get_track_lyrics(self.id) try: trackAPI_gw["LYRICS"] = dz.gw.get_track_lyrics(self.id)
except GWAPIError: self.lyrics.id = "0" except GWAPIError: self.lyrics.id = "0"
if self.lyrics.id != "0": self.lyrics.parseLyrics(trackAPI["lyrics"]) if self.lyrics.id != "0": self.lyrics.parseLyrics(trackAPI_gw["LYRICS"])
# Parse Album Data # Parse Album Data
self.album = Album( self.album = Album(
alb_id = trackAPI['album']['id'], alb_id = trackAPI_gw['ALB_ID'],
title = trackAPI['album']['title'], title = trackAPI_gw['ALB_TITLE'],
pic_md5 = trackAPI['album'].get('md5_origin') pic_md5 = trackAPI_gw.get('ALB_PICTURE')
) )
# Get album Data # Get album Data
@ -113,31 +130,28 @@ class Track:
except APIError: albumAPI = None except APIError: albumAPI = None
# Get album_gw Data # Get album_gw Data
# Only gw has disk number if not albumAPI_gw:
if not albumAPI or albumAPI and not albumAPI.get('nb_disk'): try: albumAPI_gw = dz.gw.get_album(self.album.id)
try: except GWAPIError: albumAPI_gw = None
albumAPI_gw = dz.gw.get_album(self.album.id)
albumAPI_gw = map_album(albumAPI_gw)
except GWAPIError: albumAPI_gw = {}
if not albumAPI: albumAPI = {}
albumAPI_gw.update(albumAPI)
albumAPI = albumAPI_gw
if not albumAPI: raise AlbumDoesntExists if albumAPI:
self.album.parseAlbum(albumAPI)
self.album.parseAlbum(albumAPI) elif albumAPI_gw:
# albumAPI_gw doesn't contain the artist cover self.album.parseAlbumGW(albumAPI_gw)
# Getting artist image ID # albumAPI_gw doesn't contain the artist cover
# ex: https://e-cdns-images.dzcdn.net/images/artist/f2bc007e9133c946ac3c3907ddc5d2ea/56x56-000000-80-0-0.jpg # Getting artist image ID
if not self.album.mainArtist.pic.md5 or self.album.mainArtist.pic.md5 == "": # ex: https://e-cdns-images.dzcdn.net/images/artist/f2bc007e9133c946ac3c3907ddc5d2ea/56x56-000000-80-0-0.jpg
artistAPI = dz.api.get_artist(self.album.mainArtist.id) artistAPI = dz.api.get_artist(self.album.mainArtist.id)
self.album.mainArtist.pic.md5 = artistAPI['picture_small'][artistAPI['picture_small'].find('artist/') + 7:-24] self.album.mainArtist.pic.md5 = artistAPI['picture_small'][artistAPI['picture_small'].find('artist/') + 7:-24]
else:
raise AlbumDoesntExists
# Fill missing data # Fill missing data
if albumAPI_gw: self.album.addExtraAlbumGWData(albumAPI_gw)
if self.album.date and not self.date: self.date = self.album.date if self.album.date and not self.date: self.date = self.album.date
if 'genres' in trackAPI: if not self.album.discTotal: self.album.discTotal = albumAPI_gw.get('NUMBER_DISK', "1")
for genre in trackAPI['genres']: if not self.copyright: self.copyright = albumAPI_gw['COPYRIGHT']
if genre not in self.album.genre: self.album.genre.append(genre) self.parseTrack(trackAPI)
# Remove unwanted charaters in track name # Remove unwanted charaters in track name
# Example: track/127793 # Example: track/127793
@ -145,9 +159,9 @@ class Track:
# Make sure there is at least one artist # Make sure there is at least one artist
if len(self.artist['Main']) == 0: if len(self.artist['Main']) == 0:
self.artist['Main'] = [self.mainArtist.name] self.artist['Main'] = [self.mainArtist['name']]
self.position = trackAPI.get('position') self.position = trackAPI_gw.get('POSITION')
# Add playlist data if track is in a playlist # Add playlist data if track is in a playlist
if playlistAPI: self.playlist = Playlist(playlistAPI) if playlistAPI: self.playlist = Playlist(playlistAPI)
@ -155,54 +169,64 @@ class Track:
self.generateMainFeatStrings() self.generateMainFeatStrings()
return self return self
def parseLocalTrackData(self, trackAPI): def parseLocalTrackData(self, trackAPI_gw):
# Local tracks has only the trackAPI_gw page and # Local tracks has only the trackAPI_gw page and
# contains only the tags provided by the file # contains only the tags provided by the file
self.title = trackAPI['title'] self.title = trackAPI_gw['SNG_TITLE']
self.album = Album(title=trackAPI['album']['title']) self.album = Album(title=trackAPI_gw['ALB_TITLE'])
self.album.pic = Picture( self.album.pic = Picture(
md5 = trackAPI.get('md5_image', ""), md5 = trackAPI_gw.get('ALB_PICTURE', ""),
pic_type = "cover" pic_type = "cover"
) )
self.mainArtist = Artist(name=trackAPI['artist']['name'], role="Main") self.mainArtist = Artist(name=trackAPI_gw['ART_NAME'], role="Main")
self.artists = [trackAPI['artist']['name']] self.artists = [trackAPI_gw['ART_NAME']]
self.artist = { self.artist = {
'Main': [trackAPI['artist']['name']] 'Main': [trackAPI_gw['ART_NAME']]
} }
self.album.artist = self.artist self.album.artist = self.artist
self.album.artists = self.artists self.album.artists = self.artists
self.album.date = self.date self.album.date = self.date
self.album.mainArtist = self.mainArtist self.album.mainArtist = self.mainArtist
def parseTrack(self, trackAPI): def parseTrackGW(self, trackAPI_gw):
self.title = trackAPI['title'] self.title = trackAPI_gw['SNG_TITLE'].strip()
if trackAPI_gw.get('VERSION') and not trackAPI_gw['VERSION'].strip() in self.title:
self.title += f" {trackAPI_gw['VERSION'].strip()}"
self.discNumber = trackAPI.get('disk_number') self.discNumber = trackAPI_gw.get('DISK_NUMBER')
self.explicit = trackAPI.get('explicit_lyrics', False) self.explicit = bool(int(trackAPI_gw.get('EXPLICIT_LYRICS', "0")))
self.copyright = trackAPI.get('copyright') self.copyright = trackAPI_gw.get('COPYRIGHT')
if 'gain' in trackAPI: self.replayGain = generateReplayGainString(trackAPI['gain']) if 'GAIN' in trackAPI_gw: self.replayGain = generateReplayGainString(trackAPI_gw['GAIN'])
self.ISRC = trackAPI.get('isrc') self.ISRC = trackAPI_gw.get('ISRC')
self.trackNumber = trackAPI['track_position'] self.trackNumber = trackAPI_gw['TRACK_NUMBER']
self.contributors = trackAPI.get('song_contributors') self.contributors = trackAPI_gw['SNG_CONTRIBUTORS']
self.rank = trackAPI['rank']
self.bpm = trackAPI['bpm']
self.lyrics = Lyrics(trackAPI.get('lyrics_id', "0")) self.lyrics = Lyrics(trackAPI_gw.get('LYRICS_ID', "0"))
self.mainArtist = Artist( self.mainArtist = Artist(
art_id = trackAPI['artist']['id'], art_id = trackAPI_gw['ART_ID'],
name = trackAPI['artist']['name'], name = trackAPI_gw['ART_NAME'],
role = "Main", role = "Main",
pic_md5 = trackAPI['artist'].get('md5_image') pic_md5 = trackAPI_gw.get('ART_PICTURE')
) )
if trackAPI.get('physical_release_date'): if 'PHYSICAL_RELEASE_DATE' in trackAPI_gw:
self.date.day = trackAPI["physical_release_date"][8:10] self.date.day = trackAPI_gw["PHYSICAL_RELEASE_DATE"][8:10]
self.date.month = trackAPI["physical_release_date"][5:7] self.date.month = trackAPI_gw["PHYSICAL_RELEASE_DATE"][5:7]
self.date.year = trackAPI["physical_release_date"][0:4] self.date.year = trackAPI_gw["PHYSICAL_RELEASE_DATE"][0:4]
self.date.fixDayMonth() self.date.fixDayMonth()
for artist in trackAPI.get('contributors', []): def parseTrack(self, trackAPI):
self.bpm = trackAPI['bpm']
if not self.replayGain and 'gain' in trackAPI:
self.replayGain = generateReplayGainString(trackAPI['gain'])
if not self.explicit:
self.explicit = trackAPI['explicit_lyrics']
if not self.discNumber:
self.discNumber = trackAPI['disk_number']
for artist in trackAPI['contributors']:
isVariousArtists = str(artist['id']) == VARIOUS_ARTISTS isVariousArtists = str(artist['id']) == VARIOUS_ARTISTS
isMainArtist = artist['role'] == "Main" isMainArtist = artist['role'] == "Main"
@ -217,11 +241,6 @@ class Track:
self.artist[artist['role']] = [] self.artist[artist['role']] = []
self.artist[artist['role']].append(artist['name']) self.artist[artist['role']].append(artist['name'])
if trackAPI.get('alternative_albums'):
for album in trackAPI['alternative_albums']['data']:
if 'RIGHTS' in album and album['RIGHTS'].get('STREAM_ADS_AVAILABLE') or album['RIGHTS'].get('STREAM_SUB_AVAILABLE'):
self.albumsFallback.append(album['ALB_ID'])
def removeDuplicateArtists(self): def removeDuplicateArtists(self):
(self.artist, self.artists) = removeDuplicateArtists(self.artist, self.artists) (self.artist, self.artists) = removeDuplicateArtists(self.artist, self.artists)
@ -240,14 +259,6 @@ class Track:
if 'Featured' in self.artist: if 'Featured' in self.artist:
self.featArtistsString = "feat. "+andCommaConcat(self.artist['Featured']) self.featArtistsString = "feat. "+andCommaConcat(self.artist['Featured'])
def checkAndRenewTrackToken(self, dz):
now = datetime.now()
expiration = datetime.fromtimestamp(self.trackTokenExpiration)
if now > expiration:
newTrack = dz.gw.get_track_with_fallback(self.id)
self.trackToken = newTrack['TRACK_TOKEN']
self.trackTokenExpiration = newTrack['TRACK_TOKEN_EXPIRE']
def applySettings(self, settings): def applySettings(self, settings):
# Check if should save the playlist as a compilation # Check if should save the playlist as a compilation
@ -320,3 +331,15 @@ class Track:
self.artistsString = separator.join(self.artist['Main']) self.artistsString = separator.join(self.artist['Main'])
else: else:
self.artistsString = separator.join(self.artists) self.artistsString = separator.join(self.artists)
class TrackError(Exception):
"""Base class for exceptions in this module."""
class AlbumDoesntExists(TrackError):
pass
class MD5NotFound(TrackError):
pass
class NoDataToParse(TrackError):
pass

View File

@ -1,8 +1,6 @@
import string import string
import re
from deezer import TrackFormats from deezer import TrackFormats
import os import os
from deemix.errors import ErrorMessages
USER_AGENT_HEADER = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) " \ USER_AGENT_HEADER = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) " \
"Chrome/79.0.3945.130 Safari/537.36" "Chrome/79.0.3945.130 Safari/537.36"
@ -35,35 +33,18 @@ def changeCase(txt, case_type):
if case_type == "upper": if case_type == "upper":
return txt.upper() return txt.upper()
if case_type == "start": if case_type == "start":
txt = txt.strip().split(" ") return string.capwords(txt)
for i, word in enumerate(txt):
if word[0] in ['(', '{', '[', "'", '"']:
txt[i] = word[0] + word[1:].capitalize()
else:
txt[i] = word.capitalize()
return " ".join(txt)
if case_type == "sentence": if case_type == "sentence":
return txt.capitalize() return txt.capitalize()
return str return str
def removeFeatures(title): def removeFeatures(title):
clean = title clean = title
found = False if "(feat." in clean.lower():
pos = -1 pos = clean.lower().find("(feat.")
if re.search(r"[\s(]\(?\s?feat\.?\s", clean):
pos = re.search(r"[\s(]\(?\s?feat\.?\s", clean).start(0)
found = True
if re.search(r"[\s(]\(?\s?ft\.?\s", clean):
pos = re.search(r"[\s(]\(?\s?ft\.?\s", clean).start(0)
found = True
openBracket = clean[pos] == '(' or clean[pos+1] == '('
otherBracket = clean.find('(', pos+2)
if found:
tempTrack = clean[:pos] tempTrack = clean[:pos]
if ")" in clean and openBracket: if ")" in clean:
tempTrack += clean[clean.find(")", pos+2) + 1:] tempTrack += clean[clean.find(")", pos + 1) + 1:]
if not openBracket and otherBracket != -1:
tempTrack += f" {clean[otherBracket:]}"
clean = tempTrack.strip() clean = tempTrack.strip()
clean = ' '.join(clean.split()) clean = ' '.join(clean.split())
return clean return clean
@ -92,59 +73,3 @@ def removeDuplicateArtists(artist, artists):
for role in artist.keys(): for role in artist.keys():
artist[role] = uniqueArray(artist[role]) artist[role] = uniqueArray(artist[role])
return (artist, artists) return (artist, artists)
def formatListener(key, data=None):
if key == "startAddingArtist":
return f"Started gathering {data['name']}'s albums ({data['id']})"
if key == "finishAddingArtist":
return f"Finished gathering {data['name']}'s albums ({data['id']})"
if key == "updateQueue":
uuid = f"[{data['uuid']}]"
if data.get('downloaded'):
shortFilepath = data['downloadPath'][len(data['extrasPath']):]
return f"{uuid} Completed download of {shortFilepath}"
if data.get('failed'):
return f"{uuid} {data['data']['artist']} - {data['data']['title']} :: {data['error']}"
if data.get('progress'):
return f"{uuid} Download at {data['progress']}%"
if data.get('conversion'):
return f"{uuid} Conversion at {data['conversion']}%"
return uuid
if key == "downloadInfo":
message = data['state']
if data['state'] == "getTags": message = "Getting tags."
elif data['state'] == "gotTags": message = "Tags got."
elif data['state'] == "getBitrate": message = "Getting download URL."
elif data['state'] == "bitrateFallback": message = "Desired bitrate not found, falling back to lower bitrate."
elif data['state'] == "searchFallback": message = "This track has been searched for, result might not be 100% exact."
elif data['state'] == "gotBitrate": message = "Download URL got."
elif data['state'] == "getAlbumArt": message = "Downloading album art."
elif data['state'] == "gotAlbumArt": message = "Album art downloaded."
elif data['state'] == "downloading":
message = "Downloading track."
if data['alreadyStarted']:
message += f" Recovering download from {data['value']}."
else:
message += f" Downloading {data['value']} bytes."
elif data['state'] == "downloaded": message = "Track downloaded."
elif data['state'] == "alreadyDownloaded": message = "Track already downloaded."
elif data['state'] == "tagging": message = "Tagging track."
elif data['state'] == "tagged": message = "Track tagged."
return f"[{data['uuid']}] {data['data']['artist']} - {data['data']['title']} :: {message}"
if key == "downloadWarn":
errorMessage = ErrorMessages[data['state']]
solutionMessage = ""
if data['solution'] == 'fallback': solutionMessage = "Using fallback id."
if data['solution'] == 'search': solutionMessage = "Searching for alternative."
return f"[{data['uuid']}] {data['data']['artist']} - {data['data']['title']} :: {errorMessage} {solutionMessage}"
if key == "currentItemCancelled":
return f"Current item cancelled ({data})"
if key == "removedFromQueue":
return f"[{data}] Removed from the queue"
if key == "finishDownload":
return f"[{data}] Finished downloading"
if key == "startConversion":
return f"[{data}] Started converting"
if key == "finishConversion":
return f"[{data['uuid']}] Finished converting"
return ""

View File

@ -20,7 +20,7 @@ def generateBlowfishKey(trackId):
bfKey = "" bfKey = ""
for i in range(16): for i in range(16):
bfKey += chr(ord(idMd5[i]) ^ ord(idMd5[i + 16]) ^ ord(SECRET[i])) bfKey += chr(ord(idMd5[i]) ^ ord(idMd5[i + 16]) ^ ord(SECRET[i]))
return str.encode(bfKey) return bfKey
def decryptChunk(key, data): def decryptChunk(key, data):
return Blowfish.new(key, Blowfish.MODE_CBC, b"\x00\x01\x02\x03\x04\x05\x06\x07").decrypt(data) return Blowfish.new(key, Blowfish.MODE_CBC, b"\x00\x01\x02\x03\x04\x05\x06\x07").decrypt(data)

View File

@ -5,40 +5,28 @@ CLIENT_ID = "172365"
CLIENT_SECRET = "fb0bec7ccc063dab0417eb7b0d847f34" CLIENT_SECRET = "fb0bec7ccc063dab0417eb7b0d847f34"
def getAccessToken(email, password): def getAccessToken(email, password):
accessToken = None
password = _md5(password) password = _md5(password)
request_hash = _md5(''.join([CLIENT_ID, email, password, CLIENT_SECRET])) request_hash = _md5(''.join([CLIENT_ID, email, password, CLIENT_SECRET]))
try: response = requests.get(
response = requests.get( 'https://api.deezer.com/auth/token',
'https://api.deezer.com/auth/token', params={
params={ 'app_id': CLIENT_ID,
'app_id': CLIENT_ID, 'login': email,
'login': email, 'password': password,
'password': password, 'hash': request_hash
'hash': request_hash },
}, headers={"User-Agent": USER_AGENT_HEADER}
headers={"User-Agent": USER_AGENT_HEADER} ).json()
).json() return response.get('access_token')
accessToken = response.get('access_token')
if accessToken == "undefined": accessToken = None
except Exception:
pass
return accessToken
def getArlFromAccessToken(accessToken): def getArtFromAccessToken(accessToken):
if not accessToken: return None
arl = None
session = requests.Session() session = requests.Session()
try: session.get(
session.get( "https://api.deezer.com/platform/generic/track/3135556",
"https://api.deezer.com/platform/generic/track/3135556", headers={"Authorization": f"Bearer {accessToken}", "User-Agent": USER_AGENT_HEADER}
headers={"Authorization": f"Bearer {accessToken}", "User-Agent": USER_AGENT_HEADER} )
) response = session.get(
response = session.get( 'https://www.deezer.com/ajax/gw-light.php?method=user.getArl&input=3&api_version=1.0&api_token=null',
'https://www.deezer.com/ajax/gw-light.php?method=user.getArl&input=3&api_version=1.0&api_token=null', headers={"User-Agent": USER_AGENT_HEADER}
headers={"User-Agent": USER_AGENT_HEADER} ).json()
).json() return response.get('results')
arl = response.get('results')
except Exception:
pass
return arl

View File

@ -44,27 +44,22 @@ def getMusicFolder():
musicdata = Path(os.getenv("XDG_MUSIC_DIR")) musicdata = Path(os.getenv("XDG_MUSIC_DIR"))
musicdata = checkPath(musicdata) musicdata = checkPath(musicdata)
if (homedata / '.config' / 'user-dirs.dirs').is_file() and musicdata == "": if (homedata / '.config' / 'user-dirs.dirs').is_file() and musicdata == "":
with open(homedata / '.config' / 'user-dirs.dirs', 'r', encoding="utf-8") as f: with open(homedata / '.config' / 'user-dirs.dirs', 'r') as f:
userDirs = f.read() userDirs = f.read()
musicdata_search = re.search(r"XDG_MUSIC_DIR=\"(.*)\"", userDirs) musicdata = re.search(r"XDG_MUSIC_DIR=\"(.*)\"", userDirs).group(1)
if musicdata_search: musicdata = Path(os.path.expandvars(musicdata))
musicdata = musicdata_search.group(1) musicdata = checkPath(musicdata)
musicdata = Path(os.path.expandvars(musicdata))
musicdata = checkPath(musicdata)
if os.name == 'nt' and musicdata == "": if os.name == 'nt' and musicdata == "":
try: musicKeys = ['My Music', '{4BD8D571-6D19-48D3-BE97-422220080E43}']
musicKeys = ['My Music', '{4BD8D571-6D19-48D3-BE97-422220080E43}'] regData = os.popen(r'reg.exe query "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"').read().split('\r\n')
regData = os.popen(r'reg.exe query "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"').read().split('\r\n') for i, line in enumerate(regData):
for i, line in enumerate(regData): if line == "": continue
if line == "": continue if i == 1: continue
if i == 1: continue line = line.split(' ')
line = line.split(' ') if line[1] in musicKeys:
if line[1] in musicKeys: musicdata = Path(line[3])
musicdata = Path(line[3]) break
break musicdata = checkPath(musicdata)
musicdata = checkPath(musicdata)
except Exception:
musicdata = ""
if musicdata == "": if musicdata == "":
musicdata = homedata / 'Music' musicdata = homedata / 'Music'
musicdata = checkPath(musicdata) musicdata = checkPath(musicdata)

View File

@ -2,5 +2,5 @@ click
pycryptodomex pycryptodomex
mutagen mutagen
requests requests
deezer-py
spotipy>=2.11.0 spotipy>=2.11.0
deezer-py

View File

@ -7,7 +7,7 @@ README = (HERE / "README.md").read_text()
setup( setup(
name="deemix", name="deemix",
version="3.6.6", version="3.0.0",
description="A barebone deezer downloader library", description="A barebone deezer downloader library",
long_description=README, long_description=README,
long_description_content_type="text/markdown", long_description_content_type="text/markdown",
@ -23,10 +23,7 @@ setup(
python_requires='>=3.7', python_requires='>=3.7',
packages=find_packages(exclude=("tests",)), packages=find_packages(exclude=("tests",)),
include_package_data=True, include_package_data=True,
install_requires=["click", "pycryptodomex", "mutagen", "requests", "deezer-py>=1.3.0"], install_requires=["click", "pycryptodomex", "mutagen", "requests", "spotipy>=2.11.0", "deezer-py"],
extras_require={
"spotify": ["spotipy>=2.11.0"]
},
entry_points={ entry_points={
"console_scripts": [ "console_scripts": [
"deemix=deemix.__main__:download", "deemix=deemix.__main__:download",

View File

@ -1,21 +0,0 @@
{
pkgs ? import <nixpkgs> { },
}:
pkgs.mkShell {
buildInputs = [
pkgs.python312
pkgs.python312Packages.virtualenv
];
shellHook = ''
if [ ! -d .venv ]; then
virtualenv .venv
. .venv/bin/activate
pip install -r requirements.txt
pip install -e .
else
. .venv/bin/activate
fi
'';
}

7
updatePyPi.sh Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/env bash
rm -rd build
rm -rd dist
#python -m bump
#python -m bump deemix/__init__.py
python3 setup.py sdist bdist_wheel
python3 -m twine upload dist/*