Code cleanup with pylint
This commit is contained in:
parent
eda8fd3d13
commit
69c165e2bc
|
@ -0,0 +1,2 @@
|
|||
[MESSAGES CONTROL]
|
||||
disable=C0301,C0103,R0902,R0903,C0321,R0911,R0912,R0913,R0914,R0915,R0916
|
|
@ -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
|
||||
|
|
|
@ -73,4 +73,4 @@ def download(url, bitrate, portable, path):
|
|||
click.echo("All done!")
|
||||
|
||||
if __name__ == '__main__':
|
||||
download()
|
||||
download() # pylint: disable=E1120
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,22 +69,21 @@ 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
|
||||
|
||||
def getPreferredBitrate(track, preferredBitrate, shouldFallback, downloadObjectUUID=None, interface=None):
|
||||
|
@ -116,7 +113,7 @@ def getPreferredBitrate(track, preferredBitrate, shouldFallback, downloadObjectU
|
|||
formats = formats_non_360
|
||||
|
||||
for formatNumber, formatName in formats.items():
|
||||
if formatNumber <= int(preferredBitrate):
|
||||
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"]:
|
||||
|
@ -130,12 +127,12 @@ def getPreferredBitrate(track, preferredBitrate, shouldFallback, downloadObjectU
|
|||
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")
|
||||
logger.info("%s Fallback to lower bitrate", f"[{track.mainArtist.name} - {track.title}]")
|
||||
if interface and downloadObjectUUID:
|
||||
interface.send('queueUpdate', {
|
||||
'uuid': downloadObjectUUID,
|
||||
|
@ -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"):
|
||||
and not pic_format.startswith("jpg"):
|
||||
continue
|
||||
result['albumURLs'].append({'url': url, 'ext': format})
|
||||
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)}")
|
||||
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}")
|
||||
logger.error("%s %s", itemName, error.message)
|
||||
result = {'error': {
|
||||
'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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
class Date(object):
|
||||
class Date:
|
||||
def __init__(self, day="00", month="00", year="XXXX"):
|
||||
self.year = year
|
||||
self.month = month
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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"])
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -9,29 +9,27 @@ 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
|
||||
|
||||
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:
|
||||
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):
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -52,16 +52,15 @@ 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)
|
||||
|
||||
def generateFilename(track, settings, template):
|
||||
|
|
Loading…
Reference in New Issue