diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..cf11d0f --- /dev/null +++ b/.pylintrc @@ -0,0 +1,2 @@ +[MESSAGES CONTROL] +disable=C0301,C0103,R0902,R0903,C0321,R0911,R0912,R0913,R0914,R0915,R0916 diff --git a/deemix/__init__.py b/deemix/__init__.py index f5caa61..374cd5a 100644 --- a/deemix/__init__.py +++ b/deemix/__init__.py @@ -10,54 +10,54 @@ USER_AGENT_HEADER = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, # Returns the Resolved URL, the Type and the ID def parseLink(link): - if 'deezer.page.link' in link: link = urlopen(url).url # Resolve URL shortner + if 'deezer.page.link' in link: link = urlopen(link).url # Resolve URL shortner # Remove extra stuff if '?' in link: link = link[:link.find('?')] if '&' in link: link = link[:link.find('&')] if link.endswith('/'): link = link[:-1] # Remove last slash if present - type = None - id = None + link_type = None + link_id = None - if not 'deezer' in link: return (link, type, id) # return if not a deezer link + if not 'deezer' in link: return (link, link_type, link_id) # return if not a deezer link if '/track' in link: - type = 'track' - id = re.search("\/track\/(.+)", link).group(1) + link_type = 'track' + link_id = re.search(r"/track/(.+)", link).group(1) elif '/playlist' in link: - type = 'playlist' - id = re.search("\/playlist\/(\d+)", link).group(1) + link_type = 'playlist' + link_id = re.search(r"/playlist/(\d+)", link).group(1) elif '/album' in link: - type = 'album' - id = re.search("\/album\/(.+)", link).group(1) - elif re.search("\/artist\/(\d+)\/top_track", link): - type = 'artist_top' - id = re.search("\/artist\/(\d+)\/top_track", link).group(1) - elif re.search("\/artist\/(\d+)\/discography", link): - type = 'artist_discography' - id = re.search("\/artist\/(\d+)\/discography", link).group(1) + link_type = 'album' + link_id = re.search(r"/album/(.+)", link).group(1) + elif re.search(r"/artist/(\d+)/top_track", link): + link_type = 'artist_top' + link_id = re.search(r"/artist/(\d+)/top_track", link).group(1) + elif re.search(r"/artist/(\d+)/discography", link): + link_type = 'artist_discography' + link_id = re.search(r"/artist/(\d+)/discography", link).group(1) elif '/artist' in link: - type = 'artist' - id = re.search("\/artist\/(\d+)", link).group(1) + link_type = 'artist' + link_id = re.search(r"/artist/(\d+)", link).group(1) - return (link, type, id) + return (link, link_type, link_id) def generateDownloadObject(dz, link, bitrate): - (link, type, id) = parseLink(link) + (link, link_type, link_id) = parseLink(link) - if type == None or id == None: return None - - if type == "track": - return generateTrackItem(dz, id, bitrate) - elif type == "album": - return generateAlbumItem(dz, id, bitrate) - elif type == "playlist": - return generatePlaylistItem(dz, id, bitrate) - elif type == "artist": - return generateArtistItem(dz, id, bitrate) - elif type == "artist_discography": - return generateArtistDiscographyItem(dz, id, bitrate) - elif type == "artist_top": - return generateArtistTopItem(dz, id, bitrate) + if link_type is None or link_id is None: + return None + if link_type == "track": + return generateTrackItem(dz, link_id, bitrate) + if link_type == "album": + return generateAlbumItem(dz, link_id, bitrate) + if link_type == "playlist": + return generatePlaylistItem(dz, link_id, bitrate) + if link_type == "artist": + return generateArtistItem(dz, link_id, bitrate) + if link_type == "artist_discography": + return generateArtistDiscographyItem(dz, link_id, bitrate) + if link_type == "artist_top": + return generateArtistTopItem(dz, link_id, bitrate) return None diff --git a/deemix/__main__.py b/deemix/__main__.py index 5602e36..ca7109e 100644 --- a/deemix/__main__.py +++ b/deemix/__main__.py @@ -73,4 +73,4 @@ def download(url, bitrate, portable, path): click.echo("All done!") if __name__ == '__main__': - download() + download() # pylint: disable=E1120 diff --git a/deemix/decryption.py b/deemix/decryption.py index 867cb52..1e71acd 100644 --- a/deemix/decryption.py +++ b/deemix/decryption.py @@ -1,22 +1,24 @@ import binascii +from ssl import SSLError +from time import sleep + +import logging + from Cryptodome.Cipher import Blowfish, AES from Cryptodome.Hash import MD5 -from deemix import USER_AGENT_HEADER -from deemix.types.DownloadObjects import Single, Collection - from requests import get - -from requests.exceptions import ConnectionError, ReadTimeout -from ssl import SSLError +from requests.exceptions import ConnectionError as RequestsConnectionError, ReadTimeout from urllib3.exceptions import SSLError as u3SSLError -import logging +from deemix import USER_AGENT_HEADER +from deemix.types.DownloadObjects import Single + logger = logging.getLogger('deemix') def _md5(data): h = MD5.new() - h.update(str.encode(data) if isinstance(data, str) else data) + h.update(data.encode() if isinstance(data, str) else data) return h.hexdigest() def generateBlowfishKey(trackId): @@ -27,36 +29,35 @@ def generateBlowfishKey(trackId): bfKey += chr(ord(idMd5[i]) ^ ord(idMd5[i + 16]) ^ ord(SECRET[i])) return bfKey -def generateStreamPath(sng_id, md5, media_version, format): +def generateStreamPath(sng_id, md5, media_version, media_format): urlPart = b'\xa4'.join( - [str.encode(md5), str.encode(str(format)), str.encode(str(sng_id)), str.encode(str(media_version))]) + [md5.encode(), str(media_format).encode(), str(sng_id).encode(), str(media_version).encode()]) md5val = _md5(urlPart) - step2 = str.encode(md5val) + b'\xa4' + urlPart + b'\xa4' + step2 = md5val.encode() + b'\xa4' + urlPart + b'\xa4' step2 = step2 + (b'.' * (16 - (len(step2) % 16))) urlPart = binascii.hexlify(AES.new(b'jo6aey6haid2Teih', AES.MODE_ECB).encrypt(step2)) return urlPart.decode("utf-8") def reverseStreamPath(urlPart): step2 = AES.new(b'jo6aey6haid2Teih', AES.MODE_ECB).decrypt(binascii.unhexlify(urlPart.encode("utf-8"))) - (md5val, md5, format, sng_id, media_version, _) = step2.split(b'\xa4') - return (sng_id.decode('utf-8'), md5.decode('utf-8'), media_version.decode('utf-8'), format.decode('utf-8')) + (_, md5, media_format, sng_id, media_version, _) = step2.split(b'\xa4') + return (sng_id.decode('utf-8'), md5.decode('utf-8'), media_version.decode('utf-8'), media_format.decode('utf-8')) -def generateStreamURL(sng_id, md5, media_version, format): - urlPart = generateStreamPath(sng_id, md5, media_version, format) +def generateStreamURL(sng_id, md5, media_version, media_format): + urlPart = generateStreamPath(sng_id, md5, media_version, media_format) return "https://e-cdns-proxy-" + md5[0] + ".dzcdn.net/mobile/1/" + urlPart -def generateUnencryptedStreamURL(sng_id, md5, media_version, format): - urlPart = generateStreamPath(sng_id, md5, media_version, format) +def generateUnencryptedStreamURL(sng_id, md5, media_version, media_format): + urlPart = generateStreamPath(sng_id, md5, media_version, media_format) return "https://e-cdns-proxy-" + md5[0] + ".dzcdn.net/api/1/" + urlPart def reverseStreamURL(url): urlPart = url[url.find("/1/")+3:] - return generateStreamPath(urlPart) + return reverseStreamPath(urlPart) def streamUnencryptedTrack(outputStream, track, start=0, downloadObject=None, interface=None): headers= {'User-Agent': USER_AGENT_HEADER} chunkLength = start - percentage = 0 itemName = f"[{track.mainArtist.name} - {track.title}]" @@ -68,9 +69,9 @@ def streamUnencryptedTrack(outputStream, track, start=0, downloadObject=None, in if complete == 0: raise DownloadEmpty if start != 0: responseRange = request.headers["Content-Range"] - logger.info(f'{itemName} downloading range {responseRange}') + logger.info('%s downloading range %s', itemName, responseRange) else: - logger.info(f'{itemName} downloading {complete} bytes') + logger.info('%s downloading %s bytes', itemName, complete) for chunk in request.iter_content(2048 * 3): outputStream.write(chunk) @@ -85,51 +86,12 @@ def streamUnencryptedTrack(outputStream, track, start=0, downloadObject=None, in downloadObject.progressNext += chunkProgres downloadObject.updateProgress(interface) - except (SSLError, u3SSLError) as e: - logger.info(f'{itemName} retrying from byte {chunkLength}') - return streamUnencryptedTrack(outputStream, track, chunkLength, downloadObject, interface) - except (ConnectionError, ReadTimeout): + except (SSLError, u3SSLError): + logger.info('%s retrying from byte %s', itemName, chunkLength) + streamUnencryptedTrack(outputStream, track, chunkLength, downloadObject, interface) + except (RequestsConnectionError, ReadTimeout): sleep(2) - return streamUnencryptedTrack(outputStream, track, start, downloadObject, interface) - -def streamUnencryptedTrack(outputStream, track, start=0, downloadObject=None, interface=None): - headers= {'User-Agent': USER_AGENT_HEADER} - chunkLength = start - percentage = 0 - - itemName = f"[{track.mainArtist.name} - {track.title}]" - - try: - with get(track.downloadUrl, headers=headers, stream=True, timeout=10) as request: - request.raise_for_status() - - complete = int(request.headers["Content-Length"]) - if complete == 0: raise DownloadEmpty - if start != 0: - responseRange = request.headers["Content-Range"] - logger.info(f'{itemName} downloading range {responseRange}') - else: - logger.info(f'{itemName} downloading {complete} bytes') - - for chunk in request.iter_content(2048 * 3): - outputStream.write(chunk) - chunkLength += len(chunk) - - if downloadObject: - if isinstance(downloadObject, Single): - percentage = (chunkLength / (complete + start)) * 100 - downloadObject.progressNext = percentage - else: - chunkProgres = (len(chunk) / (complete + start)) / downloadObject.size * 100 - downloadObject.progressNext += chunkProgres - downloadObject.updateProgress(interface) - - except (SSLError, u3SSLError) as e: - logger.info(f'{itemName} retrying from byte {chunkLength}') - return streamUnencryptedTrack(outputStream, track, chunkLength, downloadObject, interface) - except (ConnectionError, ReadTimeout): - sleep(2) - return streamUnencryptedTrack(outputStream, track, start, downloadObject, interface) + streamUnencryptedTrack(outputStream, track, start, downloadObject, interface) def streamTrack(outputStream, track, start=0, downloadObject=None, interface=None): headers= {'User-Agent': USER_AGENT_HEADER} @@ -147,9 +109,9 @@ def streamTrack(outputStream, track, start=0, downloadObject=None, interface=Non if complete == 0: raise DownloadEmpty if start != 0: responseRange = request.headers["Content-Range"] - logger.info(f'{itemName} downloading range {responseRange}') + logger.info('%s downloading range %s', itemName, responseRange) else: - logger.info(f'{itemName} downloading {complete} bytes') + logger.info('%s downloading %s bytes', itemName, complete) for chunk in request.iter_content(2048 * 3): if len(chunk) >= 2048: @@ -167,12 +129,12 @@ def streamTrack(outputStream, track, start=0, downloadObject=None, interface=Non downloadObject.progressNext += chunkProgres downloadObject.updateProgress(interface) - except (SSLError, u3SSLError) as e: - logger.info(f'{itemName} retrying from byte {chunkLength}') - return streamTrack(outputStream, track, chunkLength, downloadObject, interface) - except (ConnectionError, ReadTimeout): + except (SSLError, u3SSLError): + logger.info('%s retrying from byte %s', itemName, chunkLength) + streamTrack(outputStream, track, chunkLength, downloadObject, interface) + except (RequestsConnectionError, ReadTimeout): sleep(2) - return streamTrack(outputStream, track, start, downloadObject, interface) + streamTrack(outputStream, track, start, downloadObject, interface) class DownloadEmpty(Exception): pass diff --git a/deemix/downloader.py b/deemix/downloader.py index 8f6273c..fe72009 100644 --- a/deemix/downloader.py +++ b/deemix/downloader.py @@ -1,35 +1,33 @@ -import requests -from requests import get - from concurrent.futures import ThreadPoolExecutor from time import sleep from os.path import sep as pathSep +from os import makedirs, system as execute from pathlib import Path from shlex import quote -import re import errno -from ssl import SSLError -from urllib3.exceptions import SSLError as u3SSLError -from os import makedirs +import logging +from tempfile import gettempdir +import requests +from requests import get + +from urllib3.exceptions import SSLError as u3SSLError + +from mutagen.flac import FLACNoHeaderError, error as FLACError + +from deezer import TrackFormats +from deemix import USER_AGENT_HEADER from deemix.types.DownloadObjects import Single, Collection from deemix.types.Track import Track, AlbumDoesntExists from deemix.utils.pathtemplates import generateFilename, generateFilepath, settingsRegexAlbum, settingsRegexArtist, settingsRegexPlaylistFile -from deezer import TrackFormats -from deemix import USER_AGENT_HEADER from deemix.taggers import tagID3, tagFLAC from deemix.decryption import generateUnencryptedStreamURL, streamUnencryptedTrack from deemix.settings import OverwriteOption -from mutagen.flac import FLACNoHeaderError, error as FLACError - -import logging logger = logging.getLogger('deemix') -from tempfile import gettempdir - TEMPDIR = Path(gettempdir()) / 'deemix-imgs' if not TEMPDIR.is_dir(): makedirs(TEMPDIR) @@ -71,23 +69,22 @@ def downloadImage(url, path, overwrite=OverwriteOption.DONT_OVERWRITE): pictureUrl = url[len(urlBase):] pictureSize = int(pictureUrl[:pictureUrl.find("x")]) if pictureSize > 1200: - logger.warn("Couldn't download "+str(pictureSize)+"x"+str(pictureSize)+" image, falling back to 1200x1200") + logger.warning("Couldn't download %sx%s image, falling back to 1200x1200", pictureSize, pictureSize) sleep(1) return downloadImage(urlBase+pictureUrl.replace(str(pictureSize)+"x"+str(pictureSize), '1200x1200'), path, overwrite) - logger.error("Image not found: "+url) + logger.error("Image not found: %s", url) except (requests.exceptions.ConnectionError, requests.exceptions.ChunkedEncodingError, u3SSLError) as e: - logger.error("Couldn't download Image, retrying in 5 seconds...: "+url+"\n") + logger.error("Couldn't download Image, retrying in 5 seconds...: %s", url) sleep(5) return downloadImage(url, path, overwrite) except OSError as e: - if e.errno == errno.ENOSPC: raise DownloadFailed("noSpaceLeft") - else: logger.exception(f"Error while downloading an image, you should report this to the developers: {str(e)}") + if e.errno == errno.ENOSPC: raise DownloadFailed("noSpaceLeft") from e + logger.exception("Error while downloading an image, you should report this to the developers: %s", e) except Exception as e: - logger.exception(f"Error while downloading an image, you should report this to the developers: {str(e)}") + logger.exception("Error while downloading an image, you should report this to the developers: %s", e) if path.is_file(): path.unlink() return None - else: - return path + return path def getPreferredBitrate(track, preferredBitrate, shouldFallback, downloadObjectUUID=None, interface=None): if track.localTrack: return TrackFormats.LOCAL @@ -116,36 +113,36 @@ def getPreferredBitrate(track, preferredBitrate, shouldFallback, downloadObjectU formats = formats_non_360 for formatNumber, formatName in formats.items(): - if formatNumber <= int(preferredBitrate): - 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"]: - request = requests.head( - generateUnencryptedStreamURL(track.id, track.MD5, track.mediaVersion, formatNumber), - headers={'User-Agent': USER_AGENT_HEADER}, - timeout=30 - ) - try: - request.raise_for_status() - return formatNumber - except requests.exceptions.HTTPError: # if the format is not available, Deezer returns a 403 error - pass - if not shouldFallback: - raise PreferredBitrateNotFound - else: - if not falledBack: - falledBack = True - logger.info(f"[{track.mainArtist.name} - {track.title}] Fallback to lower bitrate") - if interface and downloadObjectUUID: - interface.send('queueUpdate', { - 'uuid': downloadObjectUUID, - 'bitrateFallback': True, - 'data': { - 'id': track.id, - 'title': track.title, - 'artist': track.mainArtist.name - }, - }) + if formatNumber >= int(preferredBitrate): 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"]: + request = requests.head( + generateUnencryptedStreamURL(track.id, track.MD5, track.mediaVersion, formatNumber), + headers={'User-Agent': USER_AGENT_HEADER}, + timeout=30 + ) + try: + request.raise_for_status() + return formatNumber + except requests.exceptions.HTTPError: # if the format is not available, Deezer returns a 403 error + pass + + if not shouldFallback: + raise PreferredBitrateNotFound + if not falledBack: + falledBack = True + logger.info("%s Fallback to lower bitrate", f"[{track.mainArtist.name} - {track.title}]") + if interface and downloadObjectUUID: + interface.send('queueUpdate', { + 'uuid': downloadObjectUUID, + 'bitrateFallback': True, + 'data': { + 'id': track.id, + 'title': track.title, + 'artist': track.mainArtist.name + }, + }) if is360format: raise TrackNot360 return TrackFormats.DEFAULT @@ -178,9 +175,11 @@ class Downloader: result = {} if trackAPI_gw['SNG_ID'] == "0": raise DownloadFailed("notOnDeezer") + itemName = f"[{trackAPI_gw['ART_NAME']} - {trackAPI_gw['SNG_TITLE']}]" + # Create Track object if not track: - logger.info(f"[{trackAPI_gw['ART_NAME']} - {trackAPI_gw['SNG_TITLE']}] Getting the tags") + logger.info("%s Getting the tags", itemName) try: track = Track().parseData( dz=self.dz, @@ -189,8 +188,10 @@ class Downloader: albumAPI=albumAPI, playlistAPI=playlistAPI ) - except AlbumDoesntExists: - raise DownloadError('albumDoesntExists') + except AlbumDoesntExists as e: + raise DownloadError('albumDoesntExists') from e + + itemName = f"[{track.mainArtist.name} - {track.title}]" # Check if track not yet encoded if track.MD5 == '': raise DownloadFailed("notEncoded", track) @@ -203,16 +204,16 @@ class Downloader: self.settings['fallbackBitrate'], self.downloadObject.uuid, self.interface ) - except PreferredBitrateNotFound: - raise DownloadFailed("wrongBitrate", track) - except TrackNot360: - raise DownloadFailed("no360RA") + except PreferredBitrateNotFound as e: + raise DownloadFailed("wrongBitrate", track) from e + except TrackNot360 as e: + raise DownloadFailed("no360RA") from e track.selectedFormat = selectedFormat track.album.bitrate = selectedFormat # Generate covers URLs embeddedImageFormat = f'jpg-{self.settings["jpegImageQuality"]}' - if self.settings['embeddedArtworkPNG']: imageFormat = 'png' + if self.settings['embeddedArtworkPNG']: embeddedImageFormat = 'png' track.applySettings(self.settings, TEMPDIR, embeddedImageFormat) @@ -233,49 +234,49 @@ class Downloader: result['filename'] = str(writepath)[len(str(extrasPath))+ len(pathSep):] # Download and cache coverart - logger.info(f"[{track.mainArtist.name} - {track.title}] Getting the album cover") + logger.info("%s Getting the album cover", itemName) track.album.embeddedCoverPath = downloadImage(track.album.embeddedCoverURL, track.album.embeddedCoverPath) # Save local album art if coverPath: result['albumURLs'] = [] - for format in self.settings['localArtworkFormat'].split(","): - if format in ["png","jpg"]: - extendedFormat = format + for pic_format in self.settings['localArtworkFormat'].split(","): + if pic_format in ["png","jpg"]: + extendedFormat = pic_format if extendedFormat == "jpg": extendedFormat += f"-{self.settings['jpegImageQuality']}" url = track.album.pic.generatePictureURL(self.settings['localArtworkSize'], extendedFormat) if self.settings['tags']['savePlaylistAsCompilation'] \ and track.playlist \ and track.playlist.pic.staticUrl \ - and not format.startswith("jpg"): - continue - result['albumURLs'].append({'url': url, 'ext': format}) + and not pic_format.startswith("jpg"): + continue + result['albumURLs'].append({'url': url, 'ext': pic_format}) result['albumPath'] = coverPath result['albumFilename'] = f"{settingsRegexAlbum(self.settings['coverImageTemplate'], track.album, self.settings, track.playlist)}" # Save artist art if artistPath: result['artistURLs'] = [] - for format in self.settings['localArtworkFormat'].split(","): - if format in ["png","jpg"]: - extendedFormat = format + for pic_format in self.settings['localArtworkFormat'].split(","): + if pic_format in ["png","jpg"]: + extendedFormat = pic_format if extendedFormat == "jpg": extendedFormat += f"-{self.settings['jpegImageQuality']}" url = track.album.mainArtist.pic.generatePictureURL(self.settings['localArtworkSize'], extendedFormat) - if track.album.mainArtist.pic.md5 == "" and not format.startswith("jpg"): continue - result['artistURLs'].append({'url': url, 'ext': format}) + if track.album.mainArtist.pic.md5 == "" and not pic_format.startswith("jpg"): continue + result['artistURLs'].append({'url': url, 'ext': pic_format}) result['artistPath'] = artistPath result['artistFilename'] = f"{settingsRegexArtist(self.settings['artistImageTemplate'], track.album.mainArtist, self.settings, rootArtist=track.album.rootArtist)}" # Save playlist art if track.playlist: - if not len(self.playlistURLs): - for format in self.settings['localArtworkFormat'].split(","): - if format in ["png","jpg"]: - extendedFormat = format + if self.playlistURLs == []: + for pic_format in self.settings['localArtworkFormat'].split(","): + if pic_format in ["png","jpg"]: + extendedFormat = pic_format if extendedFormat == "jpg": extendedFormat += f"-{self.settings['jpegImageQuality']}" url = track.playlist.pic.generatePictureURL(self.settings['localArtworkSize'], extendedFormat) - if track.playlist.pic.staticUrl and not format.startswith("jpg"): continue - self.playlistURLs.append({'url': url, 'ext': format}) + if track.playlist.pic.staticUrl and not pic_format.startswith("jpg"): continue + self.playlistURLs.append({'url': url, 'ext': pic_format}) if not self.playlistCoverName: track.playlist.bitrate = selectedFormat track.playlist.dateString = track.playlist.date.format(self.settings['dateFormat']) @@ -309,26 +310,26 @@ class Downloader: writepath = Path(currentFilename) if not trackAlreadyDownloaded or self.settings['overwriteFile'] == OverwriteOption.OVERWRITE: - logger.info(f"[{track.mainArtist.name} - {track.title}] Downloading the track") + logger.info("%s Downloading the track", itemName) track.downloadUrl = generateUnencryptedStreamURL(track.id, track.MD5, track.mediaVersion, track.selectedFormat) def downloadMusic(track, trackAPI_gw): try: with open(writepath, 'wb') as stream: streamUnencryptedTrack(stream, track, downloadObject=self.downloadObject, interface=self.interface) - except DownloadCancelled: + except DownloadCancelled as e: if writepath.is_file(): writepath.unlink() - raise DownloadCancelled - except (requests.exceptions.HTTPError, DownloadEmpty): + raise e + except (requests.exceptions.HTTPError, DownloadEmpty) as e: if writepath.is_file(): writepath.unlink() if track.fallbackID != "0": - logger.warn(f"[{track.mainArtist.name} - {track.title}] Track not available, using fallback id") + logger.warning("%s Track not available, using fallback id", itemName) newTrack = self.dz.gw.get_track_with_fallback(track.fallbackID) track.parseEssentialData(newTrack) track.retriveFilesizes(self.dz) return False - elif not track.searched and self.settings['fallbackSearch']: - logger.warn(f"[{track.mainArtist.name} - {track.title}] Track not available, searching for alternative") + if not track.searched and self.settings['fallbackSearch']: + logger.warning("%s Track not available, searching for alternative", itemName) searchedId = self.dz.api.get_track_id_from_metadata(track.mainArtist.name, track.title, track.album.title) if searchedId != "0": newTrack = self.dz.gw.get_track_with_fallback(searchedId) @@ -346,25 +347,21 @@ class Downloader: }, }) return False - else: - raise DownloadFailed("notAvailableNoAlternative") - else: - raise DownloadFailed("notAvailable") + raise DownloadFailed("notAvailableNoAlternative") from e + raise DownloadFailed("notAvailable") from e except (requests.exceptions.ConnectionError, requests.exceptions.ChunkedEncodingError) as e: if writepath.is_file(): writepath.unlink() - logger.warn(f"[{track.mainArtist.name} - {track.title}] Error while downloading the track, trying again in 5s...") + logger.warning("%s Error while downloading the track, trying again in 5s...", itemName) sleep(5) return downloadMusic(track, trackAPI_gw) except OSError as e: - if e.errno == errno.ENOSPC: - raise DownloadFailed("noSpaceLeft") - else: - if writepath.is_file(): writepath.unlink() - logger.exception(f"[{track.mainArtist.name} - {track.title}] Error while downloading the track, you should report this to the developers: {str(e)}") - raise e + if writepath.is_file(): writepath.unlink() + if e.errno == errno.ENOSPC: raise DownloadFailed("noSpaceLeft") from e + logger.exception("%s Error while downloading the track, you should report this to the developers: %s", itemName, e) + raise e except Exception as e: if writepath.is_file(): writepath.unlink() - logger.exception(f"[{track.mainArtist.name} - {track.title}] Error while downloading the track, you should report this to the developers: {str(e)}") + logger.exception("%s Error while downloading the track, you should report this to the developers: %s", itemName, e) raise e return True @@ -375,12 +372,12 @@ class Downloader: if not trackDownloaded: return self.download(trackAPI_gw, track=track) else: - logger.info(f"[{track.mainArtist.name} - {track.title}] Skipping track as it's already downloaded") + logger.info("%s Skipping track as it's already downloaded", itemName) self.downloadObject.completeTrackProgress(self.interface) # Adding tags if (not trackAlreadyDownloaded or self.settings['overwriteFile'] in [OverwriteOption.ONLY_TAGS, OverwriteOption.OVERWRITE]) and not track.localTrack: - logger.info(f"[{track.mainArtist.name} - {track.title}] Applying tags to the track") + logger.info("%s Applying tags to the track", itemName) if track.selectedFormat in [TrackFormats.MP3_320, TrackFormats.MP3_128, TrackFormats.DEFAULT]: tagID3(writepath, track, self.settings['tags']) elif track.selectedFormat == TrackFormats.FLAC: @@ -388,14 +385,14 @@ class Downloader: tagFLAC(writepath, track, self.settings['tags']) except (FLACNoHeaderError, FLACError): if writepath.is_file(): writepath.unlink() - logger.warn(f"[{track.mainArtist.name} - {track.title}] Track not available in FLAC, falling back if necessary") + logger.warning("%s Track not available in FLAC, falling back if necessary", itemName) self.downloadObject.removeTrackProgress(self.interface) track.filesizes['FILESIZE_FLAC'] = "0" track.filesizes['FILESIZE_FLAC_TESTED'] = True return self.download(trackAPI_gw, track=track) if track.searched: result['searched'] = f"{track.mainArtist.name} - {track.title}" - logger.info(f"[{track.mainArtist.name} - {track.title}] Track download completed\n{str(writepath)}") + logger.info("%s Track download completed\n%s", itemName, writepath) self.downloadObject.downloaded += 1 self.downloadObject.files.append(str(writepath)) self.downloadObject.extrasPath = str(self.extrasPath) @@ -413,19 +410,21 @@ class Downloader: if trackAPI_gw.get('VERSION') and trackAPI_gw['VERSION'] not in trackAPI_gw['SNG_TITLE']: tempTrack['title'] += f" {trackAPI_gw['VERSION']}".strip() + itemName = f"[{track.mainArtist.name} - {track.title}]" + try: result = self.download(trackAPI_gw, trackAPI, albumAPI, playlistAPI, track) except DownloadFailed as error: if error.track: track = error.track if track.fallbackID != "0": - logger.warn(f"[{track.mainArtist.name} - {track.title}] {error.message} Using fallback id") + logger.warning("%s %s Using fallback id", itemName, error.message) newTrack = self.dz.gw.get_track_with_fallback(track.fallbackID) track.parseEssentialData(newTrack) track.retriveFilesizes(self.dz) return self.downloadWrapper(trackAPI_gw, trackAPI, albumAPI, playlistAPI, track) - elif not track.searched and self.settings['fallbackSearch']: - logger.warn(f"[{track.mainArtist.name} - {track.title}] {error.message} Searching for alternative") + if not track.searched and self.settings['fallbackSearch']: + 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) if searchedId != "0": newTrack = self.dz.gw.get_track_with_fallback(searchedId) @@ -434,7 +433,7 @@ class Downloader: track.searched = True if self.interface: self.interface.send('queueUpdate', { - 'uuid': self.queueItem.uuid, + 'uuid': self.downloadObject.uuid, 'searchFallback': True, 'data': { 'id': track.id, @@ -443,17 +442,16 @@ class Downloader: }, }) return self.downloadWrapper(trackAPI_gw, trackAPI, albumAPI, playlistAPI, track) - else: - error.errid += "NoAlternative" - error.message = errorMessages[error.errid] - logger.error(f"[{tempTrack['artist']} - {tempTrack['title']}] {error.message}") + error.errid += "NoAlternative" + error.message = errorMessages[error.errid] + logger.error("%s %s", itemName, error.message) result = {'error': { - 'message': error.message, - 'errid': error.errid, - 'data': tempTrack - }} + 'message': error.message, + 'errid': error.errid, + 'data': tempTrack + }} except Exception as e: - logger.exception(f"[{tempTrack['artist']} - {tempTrack['title']}] {str(e)}") + logger.exception("%s %s", itemName, e) result = {'error': { 'message': str(e), 'data': tempTrack @@ -505,9 +503,9 @@ class Downloader: errors = "" searched = "" - for i in range(len(tracks)): + for i in enumerate(tracks): result = tracks[i].result() - if not result: return None # Check if item is cancelled + if not result: return # Check if item is cancelled # Log errors to file if result.get('error'): @@ -558,10 +556,10 @@ class Downloader: class DownloadError(Exception): """Base class for exceptions in this module.""" - pass class DownloadFailed(DownloadError): def __init__(self, errid, track=None): + super().__init__() self.errid = errid self.message = errorMessages[self.errid] self.track = track @@ -569,6 +567,9 @@ class DownloadFailed(DownloadError): class DownloadCancelled(DownloadError): pass +class DownloadEmpty(DownloadError): + pass + class PreferredBitrateNotFound(DownloadError): pass diff --git a/deemix/itemgen.py b/deemix/itemgen.py index 6087d2c..3cf9e5e 100644 --- a/deemix/itemgen.py +++ b/deemix/itemgen.py @@ -1,9 +1,15 @@ +import logging + from deemix.types.DownloadObjects import Single, Collection +from deezer.utils import map_user_playlist from deezer.api import APIError from deezer.gw import GWAPIError, LyricsStatus +logger = logging.getLogger('deemix') + class GenerationError(Exception): def __init__(self, link, message, errid=None): + super().__init__() self.link = link self.message = message self.errid = errid @@ -15,27 +21,26 @@ class GenerationError(Exception): 'errid': self.errid } -def generateTrackItem(dz, id, bitrate, trackAPI=None, albumAPI=None): +def generateTrackItem(dz, link_id, bitrate, trackAPI=None, albumAPI=None): # Check if is an isrc: url - if str(id).startswith("isrc"): + if str(link_id).startswith("isrc"): try: - trackAPI = dz.api.get_track(id) + trackAPI = dz.api.get_track(link_id) except APIError as e: - e = str(e) - raise GenerationError("https://deezer.com/track/"+str(id), f"Wrong URL: {e}") + raise GenerationError("https://deezer.com/track/"+str(link_id), f"Wrong URL: {e}") from e if 'id' in trackAPI and 'title' in trackAPI: - id = trackAPI['id'] + link_id = trackAPI['id'] else: - raise GenerationError("https://deezer.com/track/"+str(id), "Track ISRC is not available on deezer", "ISRCnotOnDeezer") + raise GenerationError("https://deezer.com/track/"+str(link_id), "Track ISRC is not available on deezer", "ISRCnotOnDeezer") # Get essential track info try: - trackAPI_gw = dz.gw.get_track_with_fallback(id) + trackAPI_gw = dz.gw.get_track_with_fallback(link_id) except GWAPIError as e: - e = str(e) message = "Wrong URL" - if "DATA_ERROR" in e: message += f": {e['DATA_ERROR']}" - raise GenerationError("https://deezer.com/track/"+str(id), message) + # TODO: FIX + # if "DATA_ERROR" in e: message += f": {e['DATA_ERROR']}" + raise GenerationError("https://deezer.com/track/"+str(link_id), message) from e title = trackAPI_gw['SNG_TITLE'].strip() if trackAPI_gw.get('VERSION') and trackAPI_gw['VERSION'] not in trackAPI_gw['SNG_TITLE']: @@ -44,7 +49,7 @@ def generateTrackItem(dz, id, bitrate, trackAPI=None, albumAPI=None): return Single({ 'type': 'track', - 'id': id, + 'id': link_id, 'bitrate': bitrate, 'title': title, 'artist': trackAPI_gw['ART_NAME'], @@ -57,19 +62,18 @@ def generateTrackItem(dz, id, bitrate, trackAPI=None, albumAPI=None): } }) -def generateAlbumItem(dz, id, bitrate, rootArtist=None): +def generateAlbumItem(dz, link_id, bitrate, rootArtist=None): # Get essential album info try: - albumAPI = dz.api.get_album(id) + albumAPI = dz.api.get_album(link_id) except APIError as e: - e = str(e) - raise GenerationError("https://deezer.com/album/"+str(id), f"Wrong URL: {e}") + raise GenerationError("https://deezer.com/album/"+str(link_id), f"Wrong URL: {e}") from e - if str(id).startswith('upc'): id = albumAPI['id'] + if str(link_id).startswith('upc'): link_id = albumAPI['id'] # Get extra info about album # This saves extra api calls when downloading - albumAPI_gw = dz.gw.get_album(id) + albumAPI_gw = dz.gw.get_album(link_id) albumAPI['nb_disk'] = albumAPI_gw['NUMBER_DISK'] albumAPI['copyright'] = albumAPI_gw['COPYRIGHT'] albumAPI['root_artist'] = rootArtist @@ -78,9 +82,9 @@ def generateAlbumItem(dz, id, bitrate, rootArtist=None): if albumAPI['nb_tracks'] == 1: return generateTrackItem(dz, albumAPI['tracks']['data'][0]['id'], bitrate, albumAPI=albumAPI) - tracksArray = dz.gw.get_album_tracks(id) + tracksArray = dz.gw.get_album_tracks(link_id) - if albumAPI['cover_small'] != None: + if albumAPI['cover_small'] is not None: cover = albumAPI['cover_small'][:-24] + '/75x75-000000-80-0-0.jpg' else: cover = f"https://e-cdns-images.dzcdn.net/images/cover/{albumAPI_gw['ALB_PICTURE']}/75x75-000000-80-0-0.jpg" @@ -97,7 +101,7 @@ def generateAlbumItem(dz, id, bitrate, rootArtist=None): return Collection({ 'type': 'album', - 'id': id, + 'id': link_id, 'bitrate': bitrate, 'title': albumAPI['title'], 'artist': albumAPI['artist']['name'], @@ -110,32 +114,31 @@ def generateAlbumItem(dz, id, bitrate, rootArtist=None): } }) -def generatePlaylistItem(dz, id, bitrate, playlistAPI=None, playlistTracksAPI=None): +def generatePlaylistItem(dz, link_id, bitrate, playlistAPI=None, playlistTracksAPI=None): if not playlistAPI: # Get essential playlist info try: - playlistAPI = dz.api.get_playlist(id) - except: + playlistAPI = dz.api.get_playlist(link_id) + except APIError: playlistAPI = None # Fallback to gw api if the playlist is private if not playlistAPI: try: - userPlaylist = dz.gw.get_playlist_page(id) + userPlaylist = dz.gw.get_playlist_page(link_id) playlistAPI = map_user_playlist(userPlaylist['DATA']) except GWAPIError as e: - e = str(e) message = "Wrong URL" - if "DATA_ERROR" in e: - message += f": {e['DATA_ERROR']}" - raise GenerationError("https://deezer.com/playlist/"+str(id), message) + # TODO: FIX + # if "DATA_ERROR" in e: message += f": {e['DATA_ERROR']}" + raise GenerationError("https://deezer.com/playlist/"+str(link_id), message) from e # Check if private playlist and owner if not playlistAPI.get('public', False) and playlistAPI['creator']['id'] != str(dz.current_user['id']): logger.warning("You can't download others private playlists.") - raise GenerationError("https://deezer.com/playlist/"+str(id), "You can't download others private playlists.", "notYourPrivatePlaylist") + raise GenerationError("https://deezer.com/playlist/"+str(link_id), "You can't download others private playlists.", "notYourPrivatePlaylist") if not playlistTracksAPI: - playlistTracksAPI = dz.gw.get_playlist_tracks(id) + playlistTracksAPI = dz.gw.get_playlist_tracks(link_id) playlistAPI['various_artist'] = dz.api.get_artist(5080) # Useful for save as compilation totalSize = len(playlistTracksAPI) @@ -148,11 +151,11 @@ def generatePlaylistItem(dz, id, bitrate, playlistAPI=None, playlistTracksAPI=No trackAPI['SIZE'] = totalSize collection.append(trackAPI) - if not 'explicit' in playlistAPI: playlistAPI['explicit'] = False + if 'explicit' not in playlistAPI: playlistAPI['explicit'] = False return Collection({ 'type': 'playlist', - 'id': id, + 'id': link_id, 'bitrate': bitrate, 'title': playlistAPI['title'], 'artist': playlistAPI['creator']['name'], @@ -165,60 +168,59 @@ def generatePlaylistItem(dz, id, bitrate, playlistAPI=None, playlistTracksAPI=No } }) -def generateArtistItem(dz, id, bitrate, interface=None): +def generateArtistItem(dz, link_id, bitrate, interface=None): # Get essential artist info try: - artistAPI = dz.api.get_artist(id) + artistAPI = dz.api.get_artist(link_id) except APIError as e: - e = str(e) - raise GenerationError("https://deezer.com/artist/"+str(id), f"Wrong URL: {e}") + raise GenerationError("https://deezer.com/artist/"+str(link_id), f"Wrong URL: {e}") from e - if interface: interface.send("startAddingArtist", {'name': artistAPI['name'], 'id': artistAPI['id']}) rootArtist = { 'id': artistAPI['id'], 'name': artistAPI['name'] } + if interface: interface.send("startAddingArtist", rootArtist) - artistDiscographyAPI = dz.gw.get_artist_discography_tabs(id, 100) + artistDiscographyAPI = dz.gw.get_artist_discography_tabs(link_id, 100) allReleases = artistDiscographyAPI.pop('all', []) albumList = [] for album in allReleases: albumList.append(generateAlbumItem(dz, album['id'], bitrate, rootArtist=rootArtist)) - if interface: interface.send("finishAddingArtist", {'name': artistAPI['name'], 'id': artistAPI['id']}) + if interface: interface.send("finishAddingArtist", rootArtist) return albumList -def generateArtistDiscographyItem(dz, id, bitrate, interface=None): +def generateArtistDiscographyItem(dz, link_id, bitrate, interface=None): # Get essential artist info try: - artistAPI = dz.api.get_artist(id) + artistAPI = dz.api.get_artist(link_id) except APIError as e: e = str(e) - raise GenerationError("https://deezer.com/artist/"+str(id)+"/discography", f"Wrong URL: {e}") + raise GenerationError("https://deezer.com/artist/"+str(link_id)+"/discography", f"Wrong URL: {e}") - if interface: interface.send("startAddingArtist", {'name': artistAPI['name'], 'id': artistAPI['id']}) rootArtist = { 'id': artistAPI['id'], 'name': artistAPI['name'] } + if interface: interface.send("startAddingArtist", rootArtist) - artistDiscographyAPI = dz.gw.get_artist_discography_tabs(id, 100) + artistDiscographyAPI = dz.gw.get_artist_discography_tabs(link_id, 100) artistDiscographyAPI.pop('all', None) # all contains albums and singles, so its all duplicates. This removes them albumList = [] - for type in artistDiscographyAPI: - for album in artistDiscographyAPI[type]: + for releaseType in artistDiscographyAPI: + for album in artistDiscographyAPI[releaseType]: albumList.append(generateAlbumItem(dz, album['id'], bitrate, rootArtist=rootArtist)) - if interface: interface.send("finishAddingArtist", {'name': artistAPI['name'], 'id': artistAPI['id']}) + if interface: interface.send("finishAddingArtist", rootArtist) return albumList -def generateArtistTopItem(dz, id, bitrate, interface=None): +def generateArtistTopItem(dz, link_id, bitrate, interface=None): # Get essential artist info try: - artistAPI = dz.api.get_artist(id) + artistAPI = dz.api.get_artist(link_id) except APIError as e: e = str(e) - raise GenerationError("https://deezer.com/artist/"+str(id)+"/top_track", f"Wrong URL: {e}") + raise GenerationError("https://deezer.com/artist/"+str(link_id)+"/top_track", f"Wrong URL: {e}") # Emulate the creation of a playlist # Can't use generatePlaylistItem directly as this is not a real playlist @@ -250,5 +252,5 @@ def generateArtistTopItem(dz, id, bitrate, interface=None): 'type': "playlist" } - artistTopTracksAPI_gw = dz.gw.get_artist_toptracks(id) + artistTopTracksAPI_gw = dz.gw.get_artist_toptracks(link_id) return generatePlaylistItem(dz, playlistAPI['id'], bitrate, playlistAPI=playlistAPI, playlistTracksAPI=artistTopTracksAPI_gw) diff --git a/deemix/settings.py b/deemix/settings.py index 9d3993f..fd46656 100644 --- a/deemix/settings.py +++ b/deemix/settings.py @@ -4,16 +4,16 @@ from os import makedirs from deezer import TrackFormats import deemix.utils.localpaths as localpaths -"""Should the lib overwrite files?""" class OverwriteOption(): + """Should the lib overwrite files?""" OVERWRITE = 'y' # Yes, overwrite the file DONT_OVERWRITE = 'n' # No, don't overwrite the file DONT_CHECK_EXT = 'e' # No, and don't check for extensions KEEP_BOTH = 'b' # No, and keep both files ONLY_TAGS = 't' # Overwrite only the tags -"""What should I do with featured artists?""" class FeaturesOption(): + """What should I do with featured artists?""" NO_CHANGE = "0" # Do nothing REMOVE_TITLE = "1" # Remove from track title REMOVE_TITLE_ALBUM = "3" # Remove from track title and album title @@ -121,13 +121,13 @@ def loadSettings(configFolder=None): def checkSettings(settings): changes = 0 - for set in DEFAULTS: - if not set in settings or type(settings[set]) != type(DEFAULTS[set]): - settings[set] = DEFAULTS[set] + for i_set in DEFAULTS: + if not i_set in settings or not isinstance(settings[i_set], DEFAULTS[i_set]): + settings[i_set] = DEFAULTS[i_set] changes += 1 - for set in DEFAULTS['tags']: - if not set in settings['tags'] or type(settings['tags'][set]) != type(DEFAULTS['tags'][set]): - settings['tags'][set] = DEFAULTS['tags'][set] + for i_set in DEFAULTS['tags']: + if not i_set in settings['tags'] or not isinstance(settings['tags'][i_set], DEFAULTS['tags'][i_set]): + settings['tags'][i_set] = DEFAULTS['tags'][i_set] changes += 1 if settings['downloadLocation'] == "": settings['downloadLocation'] = DEFAULTS['downloadLocation'] diff --git a/deemix/types/Album.py b/deemix/types/Album.py index 5c7d7f9..9161aa5 100644 --- a/deemix/types/Album.py +++ b/deemix/types/Album.py @@ -7,8 +7,8 @@ from deemix.types.Picture import Picture from deemix.types import VARIOUS_ARTISTS class Album: - def __init__(self, id="0", title="", pic_md5=""): - self.id = id + def __init__(self, alb_id="0", title="", pic_md5=""): + self.id = alb_id self.title = title self.pic = Picture(md5=pic_md5, type="cover") self.artist = {"Main": []} @@ -24,11 +24,15 @@ class Album: self.genre = [] self.barcode = "Unknown" self.label = "Unknown" + self.copyright = None self.recordType = "album" self.bitrate = 0 self.rootArtist = None self.variousArtists = None + self.playlistId = None + self.owner = None + def parseAlbum(self, albumAPI): self.title = albumAPI['title'] @@ -80,7 +84,7 @@ class Album: day = albumAPI["release_date"][8:10] month = albumAPI["release_date"][5:7] year = albumAPI["release_date"][0:4] - self.date = Date(year, month, day) + self.date = Date(day, month, year) self.discTotal = albumAPI.get('nb_disk') self.copyright = albumAPI.get('copyright') @@ -115,7 +119,7 @@ class Album: day = albumAPI_gw["PHYSICAL_RELEASE_DATE"][8:10] month = albumAPI_gw["PHYSICAL_RELEASE_DATE"][5:7] year = albumAPI_gw["PHYSICAL_RELEASE_DATE"][0:4] - self.date = Date(year, month, day) + self.date = Date(day, month, year) def makePlaylistCompilation(self, playlist): self.variousArtists = playlist.variousArtists @@ -136,8 +140,9 @@ class Album: self.pic = playlist.pic def removeDuplicateArtists(self): + """Removes duplicate artists for both artist array and artists dict""" (self.artist, self.artists) = removeDuplicateArtists(self.artist, self.artists) - # Removes featuring from the album name def getCleanTitle(self): + """Removes featuring from the album name""" return removeFeatures(self.title) diff --git a/deemix/types/Artist.py b/deemix/types/Artist.py index 2e0bb1b..576dc0a 100644 --- a/deemix/types/Artist.py +++ b/deemix/types/Artist.py @@ -2,8 +2,8 @@ from deemix.types.Picture import Picture from deemix.types import VARIOUS_ARTISTS class Artist: - def __init__(self, id="0", name="", role="", pic_md5=""): - self.id = str(id) + def __init__(self, art_id="0", name="", role="", pic_md5=""): + self.id = str(art_id) self.name = name self.pic = Picture(md5=pic_md5, type="artist") self.role = role diff --git a/deemix/types/Date.py b/deemix/types/Date.py index 061f2eb..196612c 100644 --- a/deemix/types/Date.py +++ b/deemix/types/Date.py @@ -1,4 +1,4 @@ -class Date(object): +class Date: def __init__(self, day="00", month="00", year="XXXX"): self.year = year self.month = month diff --git a/deemix/types/DownloadObjects.py b/deemix/types/DownloadObjects.py index c0e6736..d725561 100644 --- a/deemix/types/DownloadObjects.py +++ b/deemix/types/DownloadObjects.py @@ -1,4 +1,5 @@ class IDownloadObject: + """DownloadObject interface""" def __init__(self, obj): self.type = obj['type'] self.id = obj['id'] @@ -50,9 +51,9 @@ class IDownloadObject: def getSlimmedDict(self): light = self.toDict() propertiesToDelete = ['single', 'collection', 'convertable'] - for property in propertiesToDelete: - if property in light: - del light[property] + for prop in propertiesToDelete: + if prop in light: + del light[prop] return light def updateProgress(self, interface=None): diff --git a/deemix/types/Lyrics.py b/deemix/types/Lyrics.py index 8a02a4c..938e4da 100644 --- a/deemix/types/Lyrics.py +++ b/deemix/types/Lyrics.py @@ -1,6 +1,6 @@ class Lyrics: - def __init__(self, id="0"): - self.id = id + def __init__(self, lyr_id="0"): + self.id = lyr_id self.sync = "" self.unsync = "" self.syncID3 = [] @@ -11,7 +11,7 @@ class Lyrics: syncLyricsJson = lyricsAPI["LYRICS_SYNC_JSON"] timestamp = "" milliseconds = 0 - for line in range(len(syncLyricsJson)): + for line in enumerate(syncLyricsJson): if syncLyricsJson[line]["line"] != "": timestamp = syncLyricsJson[line]["lrc_timestamp"] milliseconds = int(syncLyricsJson[line]["milliseconds"]) diff --git a/deemix/types/Picture.py b/deemix/types/Picture.py index 859e333..1488dd1 100644 --- a/deemix/types/Picture.py +++ b/deemix/types/Picture.py @@ -1,25 +1,25 @@ class Picture: - def __init__(self, md5="", type="", url=None): + def __init__(self, md5="", pic_type="", url=None): self.md5 = md5 - self.type = type + self.type = pic_type self.staticUrl = url - def generatePictureURL(self, size, format): + def generatePictureURL(self, size, pic_format): if self.staticUrl: return self.staticUrl - url = "https://e-cdns-images.dzcdn.net/images/{}/{}/{}x{}".format( + url = "https://e-cdns-images.dzcdn.net/images/{}/{}/{size}x{size}".format( self.type, self.md5, - size, size + size=size ) - if format.startswith("jpg"): + if pic_format.startswith("jpg"): quality = 80 - if '-' in format: - quality = format[4:] - format = 'jpg' + if '-' in pic_format: + quality = pic_format[4:] + pic_format = 'jpg' return url + f'-000000-{quality}-0-0.jpg' - if format == 'png': + if pic_format == 'png': return url + '-none-100-0-0.png' return url+'.jpg' diff --git a/deemix/types/Playlist.py b/deemix/types/Playlist.py index 61412ee..9d85455 100644 --- a/deemix/types/Playlist.py +++ b/deemix/types/Playlist.py @@ -32,7 +32,7 @@ class Playlist: md5 = url[url.find(picType+'/') + len(picType)+1:-24] self.pic = Picture( md5 = md5, - type = picType + pic_type = picType ) else: self.pic = Picture(url = playlistAPI['picture_xl']) @@ -41,7 +41,7 @@ class Playlist: pic_md5 = playlistAPI['various_artist']['picture_small'] pic_md5 = pic_md5[pic_md5.find('artist/') + 7:-24] self.variousArtists = Artist( - id = playlistAPI['various_artist']['id'], + art_id = playlistAPI['various_artist']['id'], name = playlistAPI['various_artist']['name'], role = "Main", pic_md5 = pic_md5 diff --git a/deemix/types/Track.py b/deemix/types/Track.py index 43c814c..d7fb13f 100644 --- a/deemix/types/Track.py +++ b/deemix/types/Track.py @@ -1,5 +1,6 @@ -import requests from time import sleep +import re +import requests from deezer.gw import GWAPIError from deezer.api import APIError @@ -14,9 +15,11 @@ from deemix.types.Playlist import Playlist from deemix.types.Lyrics import Lyrics from deemix.types import VARIOUS_ARTISTS +from deemix.settings import FeaturesOption + class Track: - def __init__(self, id="0", name=""): - self.id = id + def __init__(self, sng_id="0", name=""): + self.id = sng_id self.title = name self.MD5 = "" self.mediaVersion = "" @@ -82,9 +85,9 @@ class Track: result_json = site.json() except: sleep(2) - return self.retriveFilesizes(dz) + self.retriveFilesizes(dz) if len(result_json['error']): - raise APIError(json.dumps(result_json['error'])) + raise APIError(result_json.dumps(result_json['error'])) response = result_json.get("results") filesizes = {} for key, value in response.items(): @@ -116,7 +119,7 @@ class Track: # Parse Album Data self.album = Album( - id = trackAPI_gw['ALB_ID'], + alb_id = trackAPI_gw['ALB_ID'], title = trackAPI_gw['ALB_TITLE'], pic_md5 = trackAPI_gw.get('ALB_PICTURE') ) @@ -157,7 +160,7 @@ class Track: if not len(self.artist['Main']): self.artist['Main'] = [self.mainArtist['name']] - self.singleDownload = trackAPI_gw.get('SINGLE_TRACK', False) # TODO: To change + self.singleDownload = trackAPI_gw.get('SINGLE_TRACK', False) # TODO: Change self.position = trackAPI_gw.get('POSITION') # Add playlist data if track is in a playlist @@ -173,7 +176,7 @@ class Track: self.album = Album(title=trackAPI_gw['ALB_TITLE']) self.album.pic = Picture( md5 = trackAPI_gw.get('ALB_PICTURE', ""), - type = "cover" + pic_type = "cover" ) self.mainArtist = Artist(name=trackAPI_gw['ART_NAME']) self.artists = [trackAPI_gw['ART_NAME']] @@ -188,7 +191,7 @@ class Track: def parseTrackGW(self, trackAPI_gw): self.title = trackAPI_gw['SNG_TITLE'].strip() - if trackAPI_gw.get('VERSION') and not trackAPI_gw['VERSION'].strip() in this.title: + if trackAPI_gw.get('VERSION') and not trackAPI_gw['VERSION'].strip() in self.title: self.title += f" {trackAPI_gw['VERSION'].strip()}" self.discNumber = trackAPI_gw.get('DISK_NUMBER') @@ -202,7 +205,7 @@ class Track: self.lyrics = Lyrics(trackAPI_gw.get('LYRICS_ID', "0")) self.mainArtist = Artist( - id = trackAPI_gw['ART_ID'], + art_id = trackAPI_gw['ART_ID'], name = trackAPI_gw['ART_NAME'], pic_md5 = trackAPI_gw.get('ART_PICTURE') ) @@ -257,7 +260,6 @@ class Track: self.featArtistsString = "feat. "+andCommaConcat(self.artist['Featured']) def applySettings(self, settings, TEMPDIR, embeddedImageFormat): - from deemix.settings import FeaturesOption # Check if should save the playlist as a compilation if self.playlist and settings['tags']['savePlaylistAsCompilation']: @@ -269,7 +271,8 @@ class Track: ext = self.album.embeddedCoverURL[-4:] if ext[0] != ".": ext = ".jpg" # Check for Spotify images - self.album.embeddedCoverPath = TEMPDIR / f"pl{trackAPI_gw['_EXTRA_PLAYLIST']['id']}_{settings['embeddedArtworkSize']}{ext}" + # TODO: FIX + # self.album.embeddedCoverPath = TEMPDIR / f"pl{trackAPI_gw['_EXTRA_PLAYLIST']['id']}_{settings['embeddedArtworkSize']}{ext}" else: if self.album.date: self.date = self.album.date self.album.embeddedCoverURL = self.album.pic.generatePictureURL(settings['embeddedArtworkSize'], embeddedImageFormat) @@ -290,7 +293,7 @@ class Track: self.album.artists.insert(0, artist.name) if isMainArtist or artist.name not in self.album.artist['Main'] and not isMainArtist: - if not artist.role in self.album.artist: + if artist.role not in self.album.artist: self.album.artist[artist.role] = [] self.album.artist[artist.role].insert(0, artist.name) self.album.mainArtist.save = not self.album.mainArtist.isVariousArtists() or settings['albumVariousArtists'] and self.album.mainArtist.isVariousArtists() @@ -319,9 +322,9 @@ class Track: self.mainArtist.name = changeCase(self.mainArtist.name, settings['artistCasing']) for i, artist in enumerate(self.artists): self.artists[i] = changeCase(artist, settings['artistCasing']) - for type in self.artist: - for i, artist in enumerate(self.artist[type]): - self.artist[type][i] = changeCase(artist, settings['artistCasing']) + for art_type in self.artist: + for i, artist in enumerate(self.artist[art_type]): + self.artist[art_type][i] = changeCase(artist, settings['artistCasing']) self.generateMainFeatStrings() # Generate artist tag @@ -343,7 +346,6 @@ class Track: class TrackError(Exception): """Base class for exceptions in this module.""" - pass class AlbumDoesntExists(TrackError): pass diff --git a/deemix/utils/__init__.py b/deemix/utils/__init__.py index 8f67ace..48fa9eb 100644 --- a/deemix/utils/__init__.py +++ b/deemix/utils/__init__.py @@ -9,30 +9,28 @@ def getBitrateNumberFromText(txt): txt = str(txt).lower() if txt in ['flac', 'lossless', '9']: return TrackFormats.FLAC - elif txt in ['mp3', '320', '3']: + if txt in ['mp3', '320', '3']: return TrackFormats.MP3_320 - elif txt in ['128', '1']: + if txt in ['128', '1']: return TrackFormats.MP3_128 - elif txt in ['360', '360_hq', '15']: + if txt in ['360', '360_hq', '15']: return TrackFormats.MP4_RA3 - elif txt in ['360_mq', '14']: + if txt in ['360_mq', '14']: return TrackFormats.MP4_RA2 - elif txt in ['360_lq', '13']: + if txt in ['360_lq', '13']: return TrackFormats.MP4_RA1 - else: - return None + return None -def changeCase(str, type): - if type == "lower": - return str.lower() - elif type == "upper": - return str.upper() - elif type == "start": - return string.capwords(str) - elif type == "sentence": - return str.capitalize() - else: - return str +def changeCase(txt, case_type): + if case_type == "lower": + return txt.lower() + if case_type == "upper": + return txt.upper() + if case_type == "start": + return string.capwords(txt) + if case_type == "sentence": + return txt.capitalize() + return str def removeFeatures(title): clean = title diff --git a/deemix/utils/localpaths.py b/deemix/utils/localpaths.py index e9a39b0..4250776 100644 --- a/deemix/utils/localpaths.py +++ b/deemix/utils/localpaths.py @@ -1,6 +1,8 @@ from pathlib import Path import sys import os +if os.name == 'nt': + import winreg # pylint: disable=E0401 homedata = Path.home() userdata = "" @@ -23,7 +25,6 @@ if os.getenv("DEEMIX_MUSIC_DIR"): elif os.getenv("XDG_MUSIC_DIR"): musicdata = Path(os.getenv("XDG_MUSIC_DIR")) / "deemix Music" elif os.name == 'nt': - import winreg sub_key = r'SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders' music_guid = '{4BD8D571-6D19-48D3-BE97-422220080E43}' with winreg.OpenKey(winreg.HKEY_CURRENT_USER, sub_key) as key: diff --git a/deemix/utils/pathtemplates.py b/deemix/utils/pathtemplates.py index 3d5dafe..9e48b8a 100644 --- a/deemix/utils/pathtemplates.py +++ b/deemix/utils/pathtemplates.py @@ -52,17 +52,16 @@ def antiDot(string): return string -def pad(num, max, settings): +def pad(num, max_val, settings): if int(settings['paddingSize']) == 0: - paddingSize = len(str(max)) + paddingSize = len(str(max_val)) else: paddingSize = len(str(10 ** (int(settings['paddingSize']) - 1))) if paddingSize == 1: paddingSize = 2 if settings['padTracks']: return str(num).zfill(paddingSize) - else: - return str(num) + return str(num) def generateFilename(track, settings, template): filename = template or "%artist% - %title%"