Compare commits
119 Commits
refactorin
...
main
Author | SHA1 | Date |
---|---|---|
Lukáš Kucharczyk | d26b1cfe46 | |
Lukáš Kucharczyk | 6e10a30c48 | |
Lukáš Kucharczyk | ebff41e016 | |
Lukáš Kucharczyk | c357cfb6a5 | |
Lukáš Kucharczyk | 945c69baf0 | |
Lukáš Kucharczyk | 1dc0d470dd | |
RemixDev | 5f978acec7 | |
RemixDev | 9162b764e7 | |
RemixDev | fc9a205662 | |
RemixDev | 203ec1f10f | |
RemixDev | b524172d5a | |
RemixDev | 463289ac46 | |
omtinez | 7fe430b77f | |
RemixDev | 27219d0698 | |
RemixDev | 1863557f49 | |
RemixDev | b983712086 | |
RemixDev | 3aa1389eed | |
omtinez | a5f4235128 | |
RemixDev | 724a7affa6 | |
RemixDev | 9e992af9c1 | |
RemixDev | e77791fe69 | |
RemixDev | 0c4db05db1 | |
RemixDev | 5107bde5c9 | |
RemixDev | 32a31d8842 | |
RemixDev | e94a3ab28f | |
RemixDev | 9f9433d205 | |
RemixDev | 5d44c971f7 | |
RemixDev | eb84a392ba | |
RemixDev | 6b41e6cb0a | |
RemixDev | ef3c9fbf57 | |
RemixDev | 55383332f8 | |
RemixDev | add5158022 | |
RemixDev | 5ec49663e3 | |
RemixDev | 6a6ec400db | |
RemixDev | 8be934fb42 | |
RemixDev | 13efa2bc90 | |
RemixDev | 133314f481 | |
RemixDev | 09511fb379 | |
RemixDev | 7f1eb0c500 | |
RemixDev | ed22a396c8 | |
RemixDev | 9906043b31 | |
RemixDev | 3b0763eeb1 | |
RemixDev | 7e6202b7f0 | |
RemixDev | 54374c87e2 | |
RemixDev | ce98393683 | |
RemixDev | d2e0450ace | |
RemixDev | 0907586e0d | |
digitalec | bb8bb1b4c4 | |
RemixDev | 45910742c2 | |
RemixDev | 09857b1247 | |
RemixDev | 93fa5fd8a1 | |
RemixDev | 114ce94148 | |
RemixDev | b9da32b2a2 | |
RemixDev | a4b707bd88 | |
RemixDev | 58f2b875fa | |
RemixDev | c2b19eef33 | |
RemixDev | 8b896fe7e7 | |
RemixDev | d11843c733 | |
RemixDev | 8074cf06b7 | |
RemixDev | 4e16f14ffc | |
RemixDev | e61a9aa626 | |
RemixDev | ae5243288d | |
Eddy Hintze | d8580d5d19 | |
RemixDev | c8bda282d1 | |
RemixDev | 2694b05b9d | |
RemixDev | 385cdce2c0 | |
RemixDev | 5d1102c6a7 | |
RemixDev | a705794a91 | |
RemixDev | caee30f37c | |
RemixDev | 242465eb21 | |
J0J0 T | c4f11aef7c | |
TheoD02 | bb00dd218d | |
RemixDev | 01bcd9ce37 | |
RemixDev | 8d84b9f85a | |
RemixDev | 87e83e807f | |
RemixDev | 2d3d6d0699 | |
RemixDev | 09f087484d | |
TheoD02 | 4c119447f5 | |
RemixDev | 44d018a810 | |
RemixDev | 41469cee64 | |
RemixDev | 8cea4289d1 | |
RemixDev | ec3a6de8df | |
RemixDev | 3030140e15 | |
RemixDev | 1859f07842 | |
RemixDev | 2e7c5c4e65 | |
RemixDev | e4f677e6b4 | |
RemixDev | 8d325d5321 | |
RemixDev | 834de5a09c | |
RemixDev | 0b8647845c | |
RemixDev | 43112e28eb | |
RemixDev | 8894ba7862 | |
RemixDev | 49dddea45e | |
RemixDev | 3b3146f220 | |
RemixDev | d0cf20db8f | |
RemixDev | 7536597495 | |
RemixDev | 4d5ef2850e | |
RemixDev | 7efaa6aaf7 | |
RemixDev | aa69439967 | |
RemixDev | 90b3be50e9 | |
RemixDev | 7838d4eefe | |
RemixDev | e11c69101b | |
RemixDev | 092e96f4bd | |
RemixDev | c007d39e15 | |
RemixDev | d80702e949 | |
RemixDev | e194588d39 | |
RemixDev | a7dd659e22 | |
RemixDev | 4119617c6b | |
RemixDev | 263ecd4be0 | |
RemixDev | 11447d606b | |
RemixDev | 536caee401 | |
RemixDev | 07bdca4599 | |
RemixDev | 4e1485f8d6 | |
RemixDev | f0c3152ffa | |
RemixDev | 78804710d1 | |
RemixDev | 82bbb5c2ab | |
RemixDev | 01cc9f5199 | |
RemixDev | b8e8d27357 | |
RemixDev | d8ecb244f5 | |
RemixDev | f530a4e89f |
|
@ -26,11 +26,13 @@ yarn-error.log*
|
|||
*.sw?
|
||||
|
||||
# Private configs
|
||||
/config.py
|
||||
/test.py
|
||||
config.py
|
||||
test.py
|
||||
config
|
||||
|
||||
#build files
|
||||
/build
|
||||
/*egg-info
|
||||
build
|
||||
*egg-info
|
||||
updatePyPi.sh
|
||||
/deezer
|
||||
deezer
|
||||
.cache
|
||||
|
|
|
@ -7,11 +7,10 @@ from deemix.itemgen import generateTrackItem, \
|
|||
generatePlaylistItem, \
|
||||
generateArtistItem, \
|
||||
generateArtistDiscographyItem, \
|
||||
generateArtistTopItem, \
|
||||
LinkNotRecognized, \
|
||||
LinkNotSupported
|
||||
generateArtistTopItem
|
||||
from deemix.errors import LinkNotRecognized, LinkNotSupported
|
||||
|
||||
__version__ = "3.0.0"
|
||||
__version__ = "3.6.6"
|
||||
|
||||
# Returns the Resolved URL, the Type and the ID
|
||||
def parseLink(link):
|
||||
|
|
|
@ -7,9 +7,21 @@ from deezer import TrackFormats
|
|||
|
||||
from deemix import generateDownloadObject
|
||||
from deemix.settings import load as loadSettings
|
||||
from deemix.utils import getBitrateNumberFromText
|
||||
from deemix.utils import getBitrateNumberFromText, formatListener
|
||||
import deemix.utils.localpaths as localpaths
|
||||
from deemix.downloader import Downloader
|
||||
from deemix.itemgen import GenerationError
|
||||
try:
|
||||
from deemix.plugins.spotify import Spotify
|
||||
except ImportError:
|
||||
Spotify = None
|
||||
|
||||
class LogListener:
|
||||
@classmethod
|
||||
def send(cls, key, value=None):
|
||||
logString = formatListener(key, value)
|
||||
if logString: print(logString)
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option('--portable', is_flag=True, help='Creates the config folder in the same directory where the script is launched')
|
||||
|
@ -22,7 +34,8 @@ def download(url, bitrate, portable, path):
|
|||
configFolder = localpath / 'config' if portable else localpaths.getConfigFolder()
|
||||
|
||||
settings = loadSettings(configFolder)
|
||||
dz = Deezer(settings.get('tagsLanguage', ""))
|
||||
dz = Deezer()
|
||||
listener = LogListener()
|
||||
|
||||
def requestValidArl():
|
||||
while True:
|
||||
|
@ -31,12 +44,22 @@ def download(url, bitrate, portable, path):
|
|||
return arl
|
||||
|
||||
if (configFolder / '.arl').is_file():
|
||||
with open(configFolder / '.arl', 'r') as f:
|
||||
with open(configFolder / '.arl', 'r', encoding="utf-8") as f:
|
||||
arl = f.readline().rstrip("\n").strip()
|
||||
if not dz.login_via_arl(arl): arl = requestValidArl()
|
||||
else: arl = requestValidArl()
|
||||
with open(configFolder / '.arl', 'w') as f:
|
||||
f.write(arl)
|
||||
try:
|
||||
with open(configFolder / '.arl', 'w', encoding="utf-8") as f:
|
||||
f.write(arl)
|
||||
except:
|
||||
print(f"Error opening {configFolder / '.arl'}, continuing anyway.")
|
||||
|
||||
plugins = {}
|
||||
if Spotify:
|
||||
plugins = {
|
||||
"spotify": Spotify(configFolder=configFolder)
|
||||
}
|
||||
plugins["spotify"].setup()
|
||||
|
||||
def downloadLinks(url, bitrate=None):
|
||||
if not bitrate: bitrate = settings.get("maxBitrate", TrackFormats.MP3_320)
|
||||
|
@ -48,9 +71,24 @@ def download(url, bitrate, portable, path):
|
|||
else:
|
||||
links.append(link)
|
||||
|
||||
downloadObjects = []
|
||||
|
||||
for link in links:
|
||||
downloadObject = generateDownloadObject(dz, link, bitrate)
|
||||
Downloader(dz, downloadObject, settings).start()
|
||||
try:
|
||||
downloadObject = generateDownloadObject(dz, link, bitrate, plugins, listener)
|
||||
except GenerationError as e:
|
||||
print(f"{e.link}: {e.message}")
|
||||
continue
|
||||
if isinstance(downloadObject, list):
|
||||
downloadObjects += downloadObject
|
||||
else:
|
||||
downloadObjects.append(downloadObject)
|
||||
|
||||
for obj in downloadObjects:
|
||||
if obj.__type__ == "Convertable":
|
||||
obj = plugins[obj.plugin].convert(dz, obj, settings, listener)
|
||||
Downloader(dz, obj, settings, listener).start()
|
||||
|
||||
|
||||
if path is not None:
|
||||
if path == '': path = '.'
|
||||
|
@ -66,7 +104,7 @@ def download(url, bitrate, portable, path):
|
|||
isfile = False
|
||||
if isfile:
|
||||
filename = url[0]
|
||||
with open(filename) as f:
|
||||
with open(filename, encoding="utf-8") as f:
|
||||
url = f.readlines()
|
||||
|
||||
downloadLinks(url, bitrate)
|
||||
|
|
|
@ -10,6 +10,7 @@ from deemix.utils.crypto import _md5, _ecbCrypt, _ecbDecrypt, generateBlowfishKe
|
|||
|
||||
from deemix.utils import USER_AGENT_HEADER
|
||||
from deemix.types.DownloadObjects import Single
|
||||
from deemix.errors import DownloadCanceled, DownloadEmpty
|
||||
|
||||
logger = logging.getLogger('deemix')
|
||||
|
||||
|
@ -40,15 +41,22 @@ def reverseStreamURL(url):
|
|||
return reverseStreamPath(urlPart)
|
||||
|
||||
def streamTrack(outputStream, track, start=0, downloadObject=None, listener=None):
|
||||
if downloadObject.isCanceled: raise DownloadCanceled
|
||||
if downloadObject and downloadObject.isCanceled: raise DownloadCanceled
|
||||
headers= {'User-Agent': USER_AGENT_HEADER}
|
||||
chunkLength = start
|
||||
isCryptedStream = "/mobile/" in track.downloadURL or "/media/" in track.downloadURL
|
||||
|
||||
itemName = f"[{track.mainArtist.name} - {track.title}]"
|
||||
itemData = {
|
||||
'id': track.id,
|
||||
'title': track.title,
|
||||
'artist': track.mainArtist.name
|
||||
}
|
||||
|
||||
try:
|
||||
with get(track.downloadUrl, headers=headers, stream=True, timeout=10) as request:
|
||||
with get(track.downloadURL, headers=headers, stream=True, timeout=10) as request:
|
||||
request.raise_for_status()
|
||||
if isCryptedStream:
|
||||
blowfish_key = generateBlowfishKey(str(track.id))
|
||||
|
||||
complete = int(request.headers["Content-Length"])
|
||||
if complete == 0: raise DownloadEmpty
|
||||
|
@ -57,7 +65,7 @@ def streamTrack(outputStream, track, start=0, downloadObject=None, listener=None
|
|||
if listener:
|
||||
listener.send('downloadInfo', {
|
||||
'uuid': downloadObject.uuid,
|
||||
'itemName': itemName,
|
||||
'data': itemData,
|
||||
'state': "downloading",
|
||||
'alreadyStarted': True,
|
||||
'value': responseRange
|
||||
|
@ -66,13 +74,24 @@ def streamTrack(outputStream, track, start=0, downloadObject=None, listener=None
|
|||
if listener:
|
||||
listener.send('downloadInfo', {
|
||||
'uuid': downloadObject.uuid,
|
||||
'itemName': itemName,
|
||||
'data': itemData,
|
||||
'state': "downloading",
|
||||
'alreadyStarted': False,
|
||||
'value': complete
|
||||
})
|
||||
|
||||
isStart = True
|
||||
for chunk in request.iter_content(2048 * 3):
|
||||
if isCryptedStream:
|
||||
if len(chunk) >= 2048:
|
||||
chunk = decryptChunk(blowfish_key, chunk[0:2048]) + chunk[2048:]
|
||||
|
||||
if isStart and chunk[0] == 0 and chunk[4:8].decode('utf-8') != "ftyp":
|
||||
for i, byte in enumerate(chunk):
|
||||
if byte != 0: break
|
||||
chunk = chunk[i:]
|
||||
isStart = False
|
||||
|
||||
outputStream.write(chunk)
|
||||
chunkLength += len(chunk)
|
||||
|
||||
|
@ -86,71 +105,7 @@ def streamTrack(outputStream, track, start=0, downloadObject=None, listener=None
|
|||
downloadObject.updateProgress(listener)
|
||||
|
||||
except (SSLError, u3SSLError):
|
||||
logger.info('%s retrying from byte %s', itemName, chunkLength)
|
||||
streamTrack(outputStream, track, chunkLength, downloadObject, listener)
|
||||
except (RequestsConnectionError, ReadTimeout, ChunkedEncodingError):
|
||||
sleep(2)
|
||||
streamTrack(outputStream, track, start, downloadObject, listener)
|
||||
|
||||
def streamCryptedTrack(outputStream, track, start=0, downloadObject=None, listener=None):
|
||||
if downloadObject.isCanceled: raise DownloadCanceled
|
||||
headers= {'User-Agent': USER_AGENT_HEADER}
|
||||
chunkLength = start
|
||||
|
||||
itemName = f"[{track.mainArtist.name} - {track.title}]"
|
||||
|
||||
try:
|
||||
with get(track.downloadUrl, headers=headers, stream=True, timeout=10) as request:
|
||||
request.raise_for_status()
|
||||
blowfish_key = str.encode(generateBlowfishKey(str(track.id)))
|
||||
|
||||
complete = int(request.headers["Content-Length"])
|
||||
if complete == 0: raise DownloadEmpty
|
||||
if start != 0:
|
||||
responseRange = request.headers["Content-Range"]
|
||||
if listener:
|
||||
listener.send('downloadInfo', {
|
||||
'uuid': downloadObject.uuid,
|
||||
'itemName': itemName,
|
||||
'state': "downloading",
|
||||
'alreadyStarted': True,
|
||||
'value': responseRange
|
||||
})
|
||||
else:
|
||||
if listener:
|
||||
listener.send('downloadInfo', {
|
||||
'uuid': downloadObject.uuid,
|
||||
'itemName': itemName,
|
||||
'state': "downloading",
|
||||
'alreadyStarted': False,
|
||||
'value': complete
|
||||
})
|
||||
|
||||
for chunk in request.iter_content(2048 * 3):
|
||||
if len(chunk) >= 2048:
|
||||
chunk = decryptChunk(blowfish_key, chunk[0:2048]) + chunk[2048:]
|
||||
|
||||
outputStream.write(chunk)
|
||||
chunkLength += len(chunk)
|
||||
|
||||
if downloadObject:
|
||||
if isinstance(downloadObject, Single):
|
||||
chunkProgres = (chunkLength / (complete + start)) * 100
|
||||
downloadObject.progressNext = chunkProgres
|
||||
else:
|
||||
chunkProgres = (len(chunk) / (complete + start)) / downloadObject.size * 100
|
||||
downloadObject.progressNext += chunkProgres
|
||||
downloadObject.updateProgress(listener)
|
||||
|
||||
except (SSLError, u3SSLError):
|
||||
logger.info('%s retrying from byte %s', itemName, chunkLength)
|
||||
streamCryptedTrack(outputStream, track, chunkLength, downloadObject, listener)
|
||||
except (RequestsConnectionError, ReadTimeout, ChunkedEncodingError):
|
||||
sleep(2)
|
||||
streamCryptedTrack(outputStream, track, start, downloadObject, listener)
|
||||
|
||||
class DownloadCanceled(Exception):
|
||||
pass
|
||||
|
||||
class DownloadEmpty(Exception):
|
||||
pass
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from concurrent.futures import ThreadPoolExecutor
|
||||
from time import sleep
|
||||
import traceback
|
||||
|
||||
from os.path import sep as pathSep
|
||||
from os import makedirs, system as execute
|
||||
|
@ -18,14 +19,17 @@ from urllib3.exceptions import SSLError as u3SSLError
|
|||
from mutagen.flac import FLACNoHeaderError, error as FLACError
|
||||
|
||||
from deezer import TrackFormats
|
||||
from deezer.errors import WrongLicense, WrongGeolocation
|
||||
from deezer.utils import map_track
|
||||
from deemix.types.DownloadObjects import Single, Collection
|
||||
from deemix.types.Track import Track, AlbumDoesntExists, MD5NotFound
|
||||
from deemix.types.Track import Track
|
||||
from deemix.types.Picture import StaticPicture
|
||||
from deemix.utils import USER_AGENT_HEADER
|
||||
from deemix.utils.pathtemplates import generatePath, generateAlbumName, generateArtistName, generateDownloadObjectName
|
||||
from deemix.tagger import tagID3, tagFLAC
|
||||
from deemix.decryption import generateStreamURL, streamTrack, DownloadCanceled
|
||||
from deemix.decryption import generateCryptedStreamURL, streamTrack
|
||||
from deemix.settings import OverwriteOption
|
||||
from deemix.errors import DownloadFailed, MD5NotFound, DownloadCanceled, PreferredBitrateNotFound, TrackNot360, AlbumDoesntExists, DownloadError, ErrorMessages
|
||||
|
||||
logger = logging.getLogger('deemix')
|
||||
|
||||
|
@ -40,6 +44,17 @@ extensions = {
|
|||
TrackFormats.MP4_RA1: '.mp4'
|
||||
}
|
||||
|
||||
formatsName = {
|
||||
TrackFormats.FLAC: 'FLAC',
|
||||
TrackFormats.LOCAL: 'MP3_MISC',
|
||||
TrackFormats.MP3_320: 'MP3_320',
|
||||
TrackFormats.MP3_128: 'MP3_128',
|
||||
TrackFormats.DEFAULT: 'MP3_MISC',
|
||||
TrackFormats.MP4_RA3: 'MP4_RA3',
|
||||
TrackFormats.MP4_RA2: 'MP4_RA2',
|
||||
TrackFormats.MP4_RA1: 'MP4_RA1'
|
||||
}
|
||||
|
||||
TEMPDIR = Path(gettempdir()) / 'deemix-imgs'
|
||||
if not TEMPDIR.is_dir(): makedirs(TEMPDIR)
|
||||
|
||||
|
@ -60,7 +75,7 @@ def downloadImage(url, path, overwrite=OverwriteOption.DONT_OVERWRITE):
|
|||
pictureSize = int(pictureUrl[:pictureUrl.find("x")])
|
||||
if pictureSize > 1200:
|
||||
return downloadImage(urlBase+pictureUrl.replace(f"{pictureSize}x{pictureSize}", '1200x1200'), path, overwrite)
|
||||
except (requests.exceptions.ConnectionError, requests.exceptions.ChunkedEncodingError, u3SSLError) as e:
|
||||
except (requests.exceptions.ConnectionError, requests.exceptions.ChunkedEncodingError, u3SSLError):
|
||||
if path.is_file(): path.unlink()
|
||||
sleep(5)
|
||||
return downloadImage(url, path, overwrite)
|
||||
|
@ -70,11 +85,54 @@ def downloadImage(url, path, overwrite=OverwriteOption.DONT_OVERWRITE):
|
|||
logger.exception("Error while downloading an image, you should report this to the developers: %s", e)
|
||||
return None
|
||||
|
||||
def getPreferredBitrate(track, bitrate, shouldFallback, uuid=None, listener=None):
|
||||
bitrate = int(bitrate)
|
||||
if track.local: return TrackFormats.LOCAL
|
||||
def getPreferredBitrate(dz, track, preferredBitrate, shouldFallback, feelingLucky, uuid=None, listener=None):
|
||||
preferredBitrate = int(preferredBitrate)
|
||||
|
||||
falledBack = False
|
||||
hasAlternative = track.fallbackID != "0"
|
||||
isGeolocked = False
|
||||
wrongLicense = False
|
||||
|
||||
def testURL(track, url, formatName):
|
||||
if not url: return False
|
||||
request = requests.head(
|
||||
url,
|
||||
headers={'User-Agent': USER_AGENT_HEADER},
|
||||
timeout=30
|
||||
)
|
||||
try:
|
||||
request.raise_for_status()
|
||||
track.filesizes[f"{formatName.lower()}"] = int(request.headers["Content-Length"])
|
||||
track.filesizes[f"{formatName.lower()}_TESTED"] = True
|
||||
return track.filesizes[f"{formatName.lower()}"] != 0
|
||||
except requests.exceptions.HTTPError: # if the format is not available, Deezer returns a 403 error
|
||||
return False
|
||||
|
||||
def getCorrectURL(track, formatName, formatNumber, feelingLucky):
|
||||
nonlocal wrongLicense, isGeolocked
|
||||
url = None
|
||||
# Check the track with the legit method
|
||||
wrongLicense = (
|
||||
(formatName == "FLAC" or formatName.startswith("MP4_RA")) and not dz.current_user.get('can_stream_lossless') or \
|
||||
formatName == "MP3_320" and not dz.current_user.get('can_stream_hq')
|
||||
)
|
||||
if track.filesizes.get(formatName.lower()) and track.filesizes[formatName.lower()] != "0":
|
||||
try:
|
||||
url = dz.get_track_url(track.trackToken, formatName)
|
||||
except (WrongLicense, WrongGeolocation) as e:
|
||||
wrongLicense = isinstance(e, WrongLicense)
|
||||
isGeolocked = isinstance(e, WrongGeolocation)
|
||||
# Fallback to old method
|
||||
if not url and feelingLucky:
|
||||
url = generateCryptedStreamURL(track.id, track.MD5, track.mediaVersion, formatNumber)
|
||||
if testURL(track, url, formatName): return url
|
||||
url = None
|
||||
return url
|
||||
|
||||
if track.local:
|
||||
url = getCorrectURL(track, "MP3_MISC", TrackFormats.LOCAL, feelingLucky)
|
||||
track.urls["MP3_MISC"] = url
|
||||
return TrackFormats.LOCAL
|
||||
|
||||
formats_non_360 = {
|
||||
TrackFormats.FLAC: "FLAC",
|
||||
|
@ -87,8 +145,7 @@ def getPreferredBitrate(track, bitrate, shouldFallback, uuid=None, listener=None
|
|||
TrackFormats.MP4_RA1: "MP4_RA1",
|
||||
}
|
||||
|
||||
is360format = bitrate in formats_360.keys()
|
||||
|
||||
is360format = preferredBitrate in formats_360.keys()
|
||||
if not shouldFallback:
|
||||
formats = formats_360
|
||||
formats.update(formats_non_360)
|
||||
|
@ -97,38 +154,41 @@ def getPreferredBitrate(track, bitrate, shouldFallback, uuid=None, listener=None
|
|||
else:
|
||||
formats = formats_non_360
|
||||
|
||||
def testBitrate(track, formatNumber, formatName):
|
||||
request = requests.head(
|
||||
generateStreamURL(track.id, track.MD5, track.mediaVersion, formatNumber),
|
||||
headers={'User-Agent': USER_AGENT_HEADER},
|
||||
timeout=30
|
||||
)
|
||||
try:
|
||||
request.raise_for_status()
|
||||
track.filesizes[f"FILESIZE_{formatName}"] = int(request.headers["Content-Length"])
|
||||
track.filesizes[f"FILESIZE_{formatName}_TESTED"] = True
|
||||
if track.filesizes[f"FILESIZE_{formatName}"] == 0: return None
|
||||
return formatNumber
|
||||
except requests.exceptions.HTTPError: # if the format is not available, Deezer returns a 403 error
|
||||
return None
|
||||
|
||||
# check and renew trackToken before starting the check
|
||||
track.checkAndRenewTrackToken(dz)
|
||||
for formatNumber, formatName in formats.items():
|
||||
if formatNumber > bitrate: continue
|
||||
if f"FILESIZE_{formatName}" in track.filesizes:
|
||||
if int(track.filesizes[f"FILESIZE_{formatName}"]) != 0: return formatNumber
|
||||
if not track.filesizes[f"FILESIZE_{formatName}_TESTED"]:
|
||||
testedBitrate = testBitrate(track, formatNumber, formatName)
|
||||
if testedBitrate: return testedBitrate
|
||||
# Current bitrate is higher than preferred bitrate; skip
|
||||
if formatNumber > preferredBitrate: continue
|
||||
|
||||
currentTrack = track
|
||||
url = getCorrectURL(currentTrack, formatName, formatNumber, feelingLucky)
|
||||
newTrack = None
|
||||
while True:
|
||||
if not url and hasAlternative:
|
||||
newTrack = dz.gw.get_track_with_fallback(currentTrack.fallbackID)
|
||||
newTrack = map_track(newTrack)
|
||||
currentTrack = Track()
|
||||
currentTrack.parseEssentialData(newTrack)
|
||||
hasAlternative = currentTrack.fallbackID != "0"
|
||||
if not url: getCorrectURL(currentTrack, formatName, formatNumber, feelingLucky)
|
||||
if (url or not hasAlternative): break
|
||||
|
||||
if url:
|
||||
if newTrack: track.parseEssentialData(newTrack)
|
||||
track.urls[formatName] = url
|
||||
return formatNumber
|
||||
|
||||
if not shouldFallback:
|
||||
if wrongLicense: raise WrongLicense(formatName)
|
||||
if isGeolocked: raise WrongGeolocation(dz.current_user['country'])
|
||||
raise PreferredBitrateNotFound
|
||||
if not falledBack:
|
||||
falledBack = True
|
||||
logger.info("%s Fallback to lower bitrate", f"[{track.mainArtist.name} - {track.title}]")
|
||||
if listener and uuid:
|
||||
listener.send('queueUpdate', {
|
||||
listener.send('downloadInfo', {
|
||||
'uuid': uuid,
|
||||
'bitrateFallback': True,
|
||||
'state': 'bitrateFallback',
|
||||
'data': {
|
||||
'id': track.id,
|
||||
'title': track.title,
|
||||
|
@ -136,6 +196,8 @@ def getPreferredBitrate(track, bitrate, shouldFallback, uuid=None, listener=None
|
|||
},
|
||||
})
|
||||
if is360format: raise TrackNot360
|
||||
url = getCorrectURL(track, "MP3_MISC", TrackFormats.DEFAULT, feelingLucky)
|
||||
track.urls["MP3_MISC"] = url
|
||||
return TrackFormats.DEFAULT
|
||||
|
||||
class Downloader:
|
||||
|
@ -146,7 +208,6 @@ class Downloader:
|
|||
self.bitrate = downloadObject.bitrate
|
||||
self.listener = listener
|
||||
|
||||
self.extrasPath = None
|
||||
self.playlistCoverName = None
|
||||
self.playlistURLs = []
|
||||
|
||||
|
@ -154,47 +215,58 @@ class Downloader:
|
|||
if not self.downloadObject.isCanceled:
|
||||
if isinstance(self.downloadObject, Single):
|
||||
track = self.downloadWrapper({
|
||||
'trackAPI_gw': self.downloadObject.single['trackAPI_gw'],
|
||||
'trackAPI': self.downloadObject.single.get('trackAPI'),
|
||||
'albumAPI': self.downloadObject.single.get('albumAPI')
|
||||
})
|
||||
if track: self.afterDownloadSingle(track)
|
||||
elif isinstance(self.downloadObject, Collection):
|
||||
tracks = [None] * len(self.downloadObject.collection['tracks_gw'])
|
||||
tracks = [None] * len(self.downloadObject.collection['tracks'])
|
||||
with ThreadPoolExecutor(self.settings['queueConcurrency']) as executor:
|
||||
for pos, track in enumerate(self.downloadObject.collection['tracks_gw'], start=0):
|
||||
for pos, track in enumerate(self.downloadObject.collection['tracks'], start=0):
|
||||
tracks[pos] = executor.submit(self.downloadWrapper, {
|
||||
'trackAPI_gw': track,
|
||||
'trackAPI': track,
|
||||
'albumAPI': self.downloadObject.collection.get('albumAPI'),
|
||||
'playlistAPI': self.downloadObject.collection.get('playlistAPI')
|
||||
})
|
||||
self.afterDownloadCollection(tracks)
|
||||
|
||||
if self.listener:
|
||||
if self.listener:
|
||||
if self.downloadObject.isCanceled:
|
||||
self.listener.send('currentItemCancelled', self.downloadObject.uuid)
|
||||
self.listener.send("removedFromQueue", self.downloadObject.uuid)
|
||||
else:
|
||||
self.listener.send("finishDownload", self.downloadObject.uuid)
|
||||
|
||||
def log(self, data, state):
|
||||
if self.listener:
|
||||
self.listener.send('downloadInfo', {'uuid': self.downloadObject.uuid, 'data': data, 'state': state})
|
||||
|
||||
def warn(self, data, state, solution):
|
||||
if self.listener:
|
||||
self.listener.send('downloadWarn', {'uuid': self.downloadObject.uuid, 'data': data, 'state': state, 'solution': solution})
|
||||
|
||||
def download(self, extraData, track=None):
|
||||
returnData = {}
|
||||
trackAPI_gw = extraData['trackAPI_gw']
|
||||
trackAPI = extraData.get('trackAPI')
|
||||
albumAPI = extraData.get('albumAPI')
|
||||
playlistAPI = extraData.get('playlistAPI')
|
||||
trackAPI['size'] = self.downloadObject.size
|
||||
if self.downloadObject.isCanceled: raise DownloadCanceled
|
||||
if trackAPI_gw['SNG_ID'] == "0": raise DownloadFailed("notOnDeezer")
|
||||
if int(trackAPI['id']) == 0: raise DownloadFailed("notOnDeezer")
|
||||
|
||||
itemName = f"[{trackAPI_gw['ART_NAME']} - {trackAPI_gw['SNG_TITLE']}]"
|
||||
itemData = {
|
||||
'id': trackAPI['id'],
|
||||
'title': trackAPI['title'],
|
||||
'artist': trackAPI['artist']['name']
|
||||
}
|
||||
|
||||
# Create Track object
|
||||
if not track:
|
||||
logger.info("%s Getting the tags", itemName)
|
||||
self.log(itemData, "getTags")
|
||||
try:
|
||||
track = Track().parseData(
|
||||
dz=self.dz,
|
||||
trackAPI_gw=trackAPI_gw,
|
||||
track_id=trackAPI['id'],
|
||||
trackAPI=trackAPI,
|
||||
albumAPI=albumAPI,
|
||||
playlistAPI=playlistAPI
|
||||
|
@ -203,26 +275,38 @@ class Downloader:
|
|||
raise DownloadError('albumDoesntExists') from e
|
||||
except MD5NotFound as e:
|
||||
raise DownloadError('notLoggedIn') from e
|
||||
self.log(itemData, "gotTags")
|
||||
|
||||
itemName = f"[{track.mainArtist.name} - {track.title}]"
|
||||
itemData = {
|
||||
'id': track.id,
|
||||
'title': track.title,
|
||||
'artist': track.mainArtist.name
|
||||
}
|
||||
|
||||
# Check if track not yet encoded
|
||||
if track.MD5 == '': raise DownloadFailed("notEncoded", track)
|
||||
|
||||
# Choose the target bitrate
|
||||
self.log(itemData, "getBitrate")
|
||||
try:
|
||||
selectedFormat = getPreferredBitrate(
|
||||
self.dz,
|
||||
track,
|
||||
self.bitrate,
|
||||
self.settings['fallbackBitrate'],
|
||||
self.settings['fallbackBitrate'], self.settings['feelingLucky'],
|
||||
self.downloadObject.uuid, self.listener
|
||||
)
|
||||
except WrongLicense as e:
|
||||
raise DownloadFailed("wrongLicense") from e
|
||||
except WrongGeolocation as e:
|
||||
raise DownloadFailed("wrongGeolocation", track) from e
|
||||
except PreferredBitrateNotFound as e:
|
||||
raise DownloadFailed("wrongBitrate", track) from e
|
||||
except TrackNot360 as e:
|
||||
raise DownloadFailed("no360RA") from e
|
||||
track.bitrate = selectedFormat
|
||||
track.album.bitrate = selectedFormat
|
||||
self.log(itemData, "gotBitrate")
|
||||
|
||||
# Apply settings
|
||||
track.applySettings(self.settings)
|
||||
|
@ -236,7 +320,7 @@ class Downloader:
|
|||
writepath = filepath / f"{filename}{extension}"
|
||||
|
||||
# Save extrasPath
|
||||
if extrasPath and not self.extrasPath: self.extrasPath = extrasPath
|
||||
if extrasPath and not self.downloadObject.extrasPath: self.downloadObject.extrasPath = extrasPath
|
||||
|
||||
# Generate covers URLs
|
||||
embeddedImageFormat = f'jpg-{self.settings["jpegImageQuality"]}'
|
||||
|
@ -248,8 +332,9 @@ class Downloader:
|
|||
track.album.embeddedCoverPath = TEMPDIR / ((f"pl{track.playlist.id}" if track.album.isPlaylist else f"alb{track.album.id}") + f"_{self.settings['embeddedArtworkSize']}{ext}")
|
||||
|
||||
# Download and cache coverart
|
||||
logger.info("%s Getting the album cover", itemName)
|
||||
self.log(itemData, "getAlbumArt")
|
||||
track.album.embeddedCoverPath = downloadImage(track.album.embeddedCoverURL, track.album.embeddedCoverPath)
|
||||
self.log(itemData, "gotAlbumArt")
|
||||
|
||||
# Save local album art
|
||||
if coverPath:
|
||||
|
@ -297,8 +382,8 @@ class Downloader:
|
|||
# Save lyrics in lrc file
|
||||
if self.settings['syncedLyrics'] and track.lyrics.sync:
|
||||
if not (filepath / f"{filename}.lrc").is_file() or self.settings['overwriteFile'] in [OverwriteOption.OVERWRITE, OverwriteOption.ONLY_TAGS]:
|
||||
with open(filepath / f"{filename}.lrc", 'wb') as f:
|
||||
f.write(track.lyrics.sync.encode('utf-8'))
|
||||
with open(filepath / f"{filename}.lrc", 'w', encoding="utf-8") as f:
|
||||
f.write(track.lyrics.sync)
|
||||
|
||||
# Check for overwrite settings
|
||||
trackAlreadyDownloaded = writepath.is_file()
|
||||
|
@ -322,26 +407,26 @@ class Downloader:
|
|||
writepath = Path(currentFilename)
|
||||
|
||||
if not trackAlreadyDownloaded or self.settings['overwriteFile'] == OverwriteOption.OVERWRITE:
|
||||
logger.info("%s Downloading the track", itemName)
|
||||
track.downloadUrl = generateStreamURL(track.id, track.MD5, track.mediaVersion, track.bitrate)
|
||||
|
||||
track.downloadURL = track.urls[formatsName[track.bitrate]]
|
||||
if not track.downloadURL: raise DownloadFailed('notAvailable', track)
|
||||
try:
|
||||
with open(writepath, 'wb') as stream:
|
||||
streamTrack(stream, track, downloadObject=self.downloadObject, listener=self.listener)
|
||||
except requests.exceptions.HTTPError as e:
|
||||
if writepath.is_file(): writepath.unlink()
|
||||
raise DownloadFailed('notAvailable', track) from e
|
||||
except OSError as e:
|
||||
if writepath.is_file(): writepath.unlink()
|
||||
if e.errno == errno.ENOSPC: raise DownloadFailed("noSpaceLeft") from e
|
||||
raise e
|
||||
|
||||
self.log(itemData, "downloaded")
|
||||
else:
|
||||
logger.info("%s Skipping track as it's already downloaded", itemName)
|
||||
self.log(itemData, "alreadyDownloaded")
|
||||
self.downloadObject.completeTrackProgress(self.listener)
|
||||
|
||||
# Adding tags
|
||||
if (not trackAlreadyDownloaded or self.settings['overwriteFile'] in [OverwriteOption.ONLY_TAGS, OverwriteOption.OVERWRITE]) and not track.local:
|
||||
logger.info("%s Applying tags to the track", itemName)
|
||||
self.log(itemData, "tagging")
|
||||
if extension == '.mp3':
|
||||
tagID3(writepath, track, self.settings['tags'])
|
||||
elif extension == '.flac':
|
||||
|
@ -349,47 +434,35 @@ class Downloader:
|
|||
tagFLAC(writepath, track, self.settings['tags'])
|
||||
except (FLACNoHeaderError, FLACError):
|
||||
writepath.unlink()
|
||||
logger.warning("%s Track not available in FLAC, falling back if necessary", itemName)
|
||||
logger.warning("%s Track not available in FLAC, falling back if necessary", f"{itemData['artist']} - {itemData['title']}")
|
||||
self.downloadObject.removeTrackProgress(self.listener)
|
||||
track.filesizes['FILESIZE_FLAC'] = "0"
|
||||
track.filesizes['FILESIZE_FLAC_TESTED'] = True
|
||||
return self.download(trackAPI_gw, track=track)
|
||||
return self.download(extraData, track=track)
|
||||
self.log(itemData, "tagged")
|
||||
|
||||
if track.searched: returnData['searched'] = True
|
||||
self.downloadObject.downloaded += 1
|
||||
self.downloadObject.files.append(str(writepath))
|
||||
self.downloadObject.extrasPath = str(self.extrasPath)
|
||||
logger.info("%s Track download completed\n%s", itemName, writepath)
|
||||
if self.listener: self.listener.send("updateQueue", {
|
||||
'uuid': self.downloadObject.uuid,
|
||||
'downloaded': True,
|
||||
'downloadPath': str(writepath),
|
||||
'extrasPath': str(self.extrasPath)
|
||||
'extrasPath': str(self.downloadObject.extrasPath)
|
||||
})
|
||||
returnData['filename'] = str(writepath)[len(str(extrasPath))+ len(pathSep):]
|
||||
returnData['data'] = {
|
||||
'id': track.id,
|
||||
'title': track.title,
|
||||
'artist': track.mainArtist.name
|
||||
}
|
||||
returnData['data'] = itemData
|
||||
returnData['path'] = str(writepath)
|
||||
self.downloadObject.files.append(returnData)
|
||||
return returnData
|
||||
|
||||
def downloadWrapper(self, extraData, track=None):
|
||||
trackAPI_gw = extraData['trackAPI_gw']
|
||||
if ('_EXTRA_TRACK' in trackAPI_gw):
|
||||
extraData['trackAPI'] = trackAPI_gw['_EXTRA_TRACK'].copy()
|
||||
del extraData['trackAPI_gw']['_EXTRA_TRACK']
|
||||
del trackAPI_gw['_EXTRA_TRACK']
|
||||
trackAPI = extraData['trackAPI']
|
||||
# Temp metadata to generate logs
|
||||
tempTrack = {
|
||||
'id': trackAPI_gw['SNG_ID'],
|
||||
'title': trackAPI_gw['SNG_TITLE'].strip(),
|
||||
'artist': trackAPI_gw['ART_NAME']
|
||||
itemData = {
|
||||
'id': trackAPI['id'],
|
||||
'title': trackAPI['title'],
|
||||
'artist': trackAPI['artist']['name']
|
||||
}
|
||||
if trackAPI_gw.get('VERSION') and trackAPI_gw['VERSION'] not in trackAPI_gw['SNG_TITLE']:
|
||||
tempTrack['title'] += f" {trackAPI_gw['VERSION']}".strip()
|
||||
|
||||
itemName = f"[{tempTrack['artist']} - {tempTrack['title']}]"
|
||||
|
||||
try:
|
||||
result = self.download(extraData, track)
|
||||
|
@ -397,42 +470,50 @@ class Downloader:
|
|||
if error.track:
|
||||
track = error.track
|
||||
if track.fallbackID != "0":
|
||||
logger.warning("%s %s Using fallback id", itemName, error.message)
|
||||
self.warn(itemData, error.errid, 'fallback')
|
||||
newTrack = self.dz.gw.get_track_with_fallback(track.fallbackID)
|
||||
newTrack = map_track(newTrack)
|
||||
track.parseEssentialData(newTrack)
|
||||
track.retriveFilesizes(self.dz)
|
||||
return self.downloadWrapper(extraData, track)
|
||||
if len(track.albumsFallback) != 0 and self.settings['fallbackISRC']:
|
||||
newAlbumID = track.albumsFallback.pop()
|
||||
newAlbum = self.dz.gw.get_album_page(newAlbumID)
|
||||
fallbackID = 0
|
||||
for newTrack in newAlbum['SONGS']['data']:
|
||||
if newTrack['ISRC'] == track.ISRC:
|
||||
fallbackID = newTrack['SNG_ID']
|
||||
break
|
||||
if fallbackID != 0:
|
||||
self.warn(itemData, error.errid, 'fallback')
|
||||
newTrack = self.dz.gw.get_track_with_fallback(fallbackID)
|
||||
newTrack = map_track(newTrack)
|
||||
track.parseEssentialData(newTrack)
|
||||
return self.downloadWrapper(extraData, track)
|
||||
if not track.searched and self.settings['fallbackSearch']:
|
||||
logger.warning("%s %s Searching for alternative", itemName, error.message)
|
||||
self.warn(itemData, error.errid, 'search')
|
||||
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)
|
||||
newTrack = map_track(newTrack)
|
||||
track.parseEssentialData(newTrack)
|
||||
track.retriveFilesizes(self.dz)
|
||||
track.searched = True
|
||||
if self.listener: self.listener.send('queueUpdate', {
|
||||
'uuid': self.downloadObject.uuid,
|
||||
'searchFallback': True,
|
||||
'data': {
|
||||
'id': track.id,
|
||||
'title': track.title,
|
||||
'artist': track.mainArtist.name
|
||||
},
|
||||
})
|
||||
self.log(itemData, "searchFallback")
|
||||
return self.downloadWrapper(extraData, track)
|
||||
error.errid += "NoAlternative"
|
||||
error.message = errorMessages[error.errid]
|
||||
logger.error("%s %s", itemName, error.message)
|
||||
error.message = ErrorMessages[error.errid]
|
||||
result = {'error': {
|
||||
'message': error.message,
|
||||
'errid': error.errid,
|
||||
'data': tempTrack
|
||||
'data': itemData,
|
||||
'type': "track"
|
||||
}}
|
||||
except Exception as e:
|
||||
logger.exception("%s %s", itemName, e)
|
||||
logger.exception("%s %s", f"{itemData['artist']} - {itemData['title']}", e)
|
||||
result = {'error': {
|
||||
'message': str(e),
|
||||
'data': tempTrack
|
||||
'data': itemData,
|
||||
'stack': traceback.format_exc(),
|
||||
'type': "track"
|
||||
}}
|
||||
|
||||
if 'error' in result:
|
||||
|
@ -446,39 +527,74 @@ class Downloader:
|
|||
'failed': True,
|
||||
'data': error['data'],
|
||||
'error': error['message'],
|
||||
'errid': error['errid'] if 'errid' in error else None
|
||||
'errid': error.get('errid'),
|
||||
'stack': error.get('stack'),
|
||||
'type': error['type']
|
||||
})
|
||||
return result
|
||||
|
||||
def afterDownloadErrorReport(self, position, error, itemData=None):
|
||||
if not itemData: itemData = {}
|
||||
data = {'position': position }
|
||||
data.update(itemData)
|
||||
logger.exception("%s %s", position, error)
|
||||
self.downloadObject.errors.append({
|
||||
'message': str(error),
|
||||
'stack': traceback.format_exc(),
|
||||
'data': data,
|
||||
'type': "post"
|
||||
})
|
||||
if self.listener:
|
||||
self.listener.send("updateQueue", {
|
||||
'uuid': self.downloadObject.uuid,
|
||||
'postFailed': True,
|
||||
'data': data,
|
||||
'error': str(error),
|
||||
'stack': traceback.format_exc(),
|
||||
'type': "post"
|
||||
})
|
||||
|
||||
def afterDownloadSingle(self, track):
|
||||
if not self.extrasPath: self.extrasPath = Path(self.settings['downloadLocation'])
|
||||
if not self.downloadObject.extrasPath: self.downloadObject.extrasPath = Path(self.settings['downloadLocation'])
|
||||
|
||||
# Save Album Cover
|
||||
if self.settings['saveArtwork'] and 'albumPath' in track:
|
||||
for image in track['albumURLs']:
|
||||
downloadImage(image['url'], track['albumPath'] / f"{track['albumFilename']}.{image['ext']}", self.settings['overwriteFile'])
|
||||
try:
|
||||
if self.settings['saveArtwork'] and 'albumPath' in track:
|
||||
for image in track['albumURLs']:
|
||||
downloadImage(image['url'], track['albumPath'] / f"{track['albumFilename']}.{image['ext']}", self.settings['overwriteFile'])
|
||||
except Exception as e:
|
||||
self.afterDownloadErrorReport("SaveLocalAlbumArt", e)
|
||||
|
||||
# Save Artist Artwork
|
||||
if self.settings['saveArtworkArtist'] and 'artistPath' in track:
|
||||
for image in track['artistURLs']:
|
||||
downloadImage(image['url'], track['artistPath'] / f"{track['artistFilename']}.{image['ext']}", self.settings['overwriteFile'])
|
||||
try:
|
||||
if self.settings['saveArtworkArtist'] and 'artistPath' in track:
|
||||
for image in track['artistURLs']:
|
||||
downloadImage(image['url'], track['artistPath'] / f"{track['artistFilename']}.{image['ext']}", self.settings['overwriteFile'])
|
||||
except Exception as e:
|
||||
self.afterDownloadErrorReport("SaveLocalArtistArt", e)
|
||||
|
||||
# Create searched logfile
|
||||
if self.settings['logSearched'] and 'searched' in track:
|
||||
filename = f"{track.data.artist} - {track.data.title}"
|
||||
with open(self.extrasPath / 'searched.txt', 'wb+') as f:
|
||||
searchedFile = f.read().decode('utf-8')
|
||||
if not filename in searchedFile:
|
||||
if searchedFile != "": searchedFile += "\r\n"
|
||||
searchedFile += filename + "\r\n"
|
||||
f.write(searchedFile.encode('utf-8'))
|
||||
try:
|
||||
if self.settings['logSearched'] and 'searched' in track:
|
||||
filename = f"{track.data.artist} - {track.data.title}"
|
||||
with open(self.downloadObject.extrasPath / 'searched.txt', 'w+', encoding="utf-8") as f:
|
||||
searchedFile = f.read()
|
||||
if not filename in searchedFile:
|
||||
if searchedFile != "": searchedFile += "\r\n"
|
||||
searchedFile += filename + "\r\n"
|
||||
f.write(searchedFile)
|
||||
except Exception as e:
|
||||
self.afterDownloadErrorReport("CreateSearchedLog", e)
|
||||
|
||||
# Execute command after download
|
||||
if self.settings['executeCommand'] != "":
|
||||
execute(self.settings['executeCommand'].replace("%folder%", quote(str(self.extrasPath))).replace("%filename%", quote(track['filename'])), shell=True)
|
||||
try:
|
||||
if self.settings['executeCommand'] != "":
|
||||
execute(self.settings['executeCommand'].replace("%folder%", quote(str(self.downloadObject.extrasPath))).replace("%filename%", quote(track['filename'])))
|
||||
except Exception as e:
|
||||
self.afterDownloadErrorReport("ExecuteCommand", e)
|
||||
|
||||
def afterDownloadCollection(self, tracks):
|
||||
if not self.extrasPath: self.extrasPath = Path(self.settings['downloadLocation'])
|
||||
if not self.downloadObject.extrasPath: self.downloadObject.extrasPath = Path(self.settings['downloadLocation'])
|
||||
playlist = [None] * len(tracks)
|
||||
errors = ""
|
||||
searched = ""
|
||||
|
@ -496,69 +612,61 @@ class Downloader:
|
|||
if 'searched' in track: searched += track['searched'] + "\r\n"
|
||||
|
||||
# Save Album Cover
|
||||
if self.settings['saveArtwork'] and 'albumPath' in track:
|
||||
for image in track['albumURLs']:
|
||||
downloadImage(image['url'], track['albumPath'] / f"{track['albumFilename']}.{image['ext']}", self.settings['overwriteFile'])
|
||||
try:
|
||||
if self.settings['saveArtwork'] and 'albumPath' in track:
|
||||
for image in track['albumURLs']:
|
||||
downloadImage(image['url'], track['albumPath'] / f"{track['albumFilename']}.{image['ext']}", self.settings['overwriteFile'])
|
||||
except Exception as e:
|
||||
self.afterDownloadErrorReport("SaveLocalAlbumArt", e, track['data'])
|
||||
|
||||
# Save Artist Artwork
|
||||
if self.settings['saveArtworkArtist'] and 'artistPath' in track:
|
||||
for image in track['artistURLs']:
|
||||
downloadImage(image['url'], track['artistPath'] / f"{track['artistFilename']}.{image['ext']}", self.settings['overwriteFile'])
|
||||
try:
|
||||
if self.settings['saveArtworkArtist'] and 'artistPath' in track:
|
||||
for image in track['artistURLs']:
|
||||
downloadImage(image['url'], track['artistPath'] / f"{track['artistFilename']}.{image['ext']}", self.settings['overwriteFile'])
|
||||
except Exception as e:
|
||||
self.afterDownloadErrorReport("SaveLocalArtistArt", e, track['data'])
|
||||
|
||||
# Save filename for playlist file
|
||||
playlist[i] = track.get('filename', "")
|
||||
|
||||
# Create errors logfile
|
||||
if self.settings['logErrors'] and errors != "":
|
||||
with open(self.extrasPath / 'errors.txt', 'wb') as f:
|
||||
f.write(errors.encode('utf-8'))
|
||||
try:
|
||||
if self.settings['logErrors'] and errors != "":
|
||||
with open(self.downloadObject.extrasPath / 'errors.txt', 'w', encoding="utf-8") as f:
|
||||
f.write(errors)
|
||||
except Exception as e:
|
||||
self.afterDownloadErrorReport("CreateErrorLog", e)
|
||||
|
||||
# Create searched logfile
|
||||
if self.settings['logSearched'] and searched != "":
|
||||
with open(self.extrasPath / 'searched.txt', 'wb') as f:
|
||||
f.write(searched.encode('utf-8'))
|
||||
try:
|
||||
if self.settings['logSearched'] and searched != "":
|
||||
with open(self.downloadObject.extrasPath / 'searched.txt', 'w', encoding="utf-8") as f:
|
||||
f.write(searched)
|
||||
except Exception as e:
|
||||
self.afterDownloadErrorReport("CreateSearchedLog", e)
|
||||
|
||||
# Save Playlist Artwork
|
||||
if self.settings['saveArtwork'] and self.playlistCoverName and not self.settings['tags']['savePlaylistAsCompilation']:
|
||||
for image in self.playlistURLs:
|
||||
downloadImage(image['url'], self.extrasPath / f"{self.playlistCoverName}.{image['ext']}", self.settings['overwriteFile'])
|
||||
try:
|
||||
if self.settings['saveArtwork'] and self.playlistCoverName and not self.settings['tags']['savePlaylistAsCompilation']:
|
||||
for image in self.playlistURLs:
|
||||
downloadImage(image['url'], self.downloadObject.extrasPath / f"{self.playlistCoverName}.{image['ext']}", self.settings['overwriteFile'])
|
||||
except Exception as e:
|
||||
self.afterDownloadErrorReport("SavePlaylistArt", e)
|
||||
|
||||
# Create M3U8 File
|
||||
if self.settings['createM3U8File']:
|
||||
filename = generateDownloadObjectName(self.settings['playlistFilenameTemplate'], self.downloadObject, self.settings) or "playlist"
|
||||
with open(self.extrasPath / f'{filename}.m3u8', 'wb') as f:
|
||||
for line in playlist:
|
||||
f.write((line + "\n").encode('utf-8'))
|
||||
try:
|
||||
if self.settings['createM3U8File']:
|
||||
filename = generateDownloadObjectName(self.settings['playlistFilenameTemplate'], self.downloadObject, self.settings) or "playlist"
|
||||
with open(self.downloadObject.extrasPath / f'{filename}.m3u8', 'w', encoding="utf-8") as f:
|
||||
for line in playlist:
|
||||
f.write(line + "\n")
|
||||
except Exception as e:
|
||||
self.afterDownloadErrorReport("CreatePlaylistFile", e)
|
||||
|
||||
# Execute command after download
|
||||
if self.settings['executeCommand'] != "":
|
||||
execute(self.settings['executeCommand'].replace("%folder%", quote(str(self.extrasPath))), shell=True)
|
||||
|
||||
class DownloadError(Exception):
|
||||
"""Base class for exceptions in this module."""
|
||||
|
||||
errorMessages = {
|
||||
'notOnDeezer': "Track not available on Deezer!",
|
||||
'notEncoded': "Track not yet encoded!",
|
||||
'notEncodedNoAlternative': "Track not yet encoded and no alternative found!",
|
||||
'wrongBitrate': "Track not found at desired bitrate.",
|
||||
'wrongBitrateNoAlternative': "Track not found at desired bitrate and no alternative found!",
|
||||
'no360RA': "Track is not available in Reality Audio 360.",
|
||||
'notAvailable': "Track not available on deezer's servers!",
|
||||
'notAvailableNoAlternative': "Track not available on deezer's servers and no alternative found!",
|
||||
'noSpaceLeft': "No space left on target drive, clean up some space for the tracks",
|
||||
'albumDoesntExists': "Track's album does not exsist, failed to gather info"
|
||||
}
|
||||
|
||||
class DownloadFailed(DownloadError):
|
||||
def __init__(self, errid, track=None):
|
||||
super().__init__()
|
||||
self.errid = errid
|
||||
self.message = errorMessages[self.errid]
|
||||
self.track = track
|
||||
|
||||
class PreferredBitrateNotFound(DownloadError):
|
||||
pass
|
||||
|
||||
class TrackNot360(DownloadError):
|
||||
pass
|
||||
try:
|
||||
if self.settings['executeCommand'] != "":
|
||||
execute(self.settings['executeCommand'].replace("%folder%", quote(str(self.downloadObject.extrasPath))))
|
||||
except Exception as e:
|
||||
self.afterDownloadErrorReport("ExecuteCommand", e)
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
class DeemixError(Exception):
|
||||
"""Base exception for this module"""
|
||||
|
||||
class GenerationError(DeemixError):
|
||||
"""Generation related errors"""
|
||||
def __init__(self, link, message, errid=None):
|
||||
super().__init__()
|
||||
self.link = link
|
||||
self.message = message
|
||||
self.errid = errid
|
||||
|
||||
def toDict(self):
|
||||
return {
|
||||
'link': self.link,
|
||||
'error': self.message,
|
||||
'errid': self.errid
|
||||
}
|
||||
|
||||
class ISRCnotOnDeezer(GenerationError):
|
||||
def __init__(self, link):
|
||||
super().__init__(link, "Track ISRC is not available on deezer", "ISRCnotOnDeezer")
|
||||
|
||||
class NotYourPrivatePlaylist(GenerationError):
|
||||
def __init__(self, link):
|
||||
super().__init__(link, "You can't download others private playlists.", "notYourPrivatePlaylist")
|
||||
|
||||
class TrackNotOnDeezer(GenerationError):
|
||||
def __init__(self, link):
|
||||
super().__init__(link, "Track not found on deezer!", "trackNotOnDeezer")
|
||||
|
||||
class AlbumNotOnDeezer(GenerationError):
|
||||
def __init__(self, link):
|
||||
super().__init__(link, "Album not found on deezer!", "albumNotOnDeezer")
|
||||
|
||||
class InvalidID(GenerationError):
|
||||
def __init__(self, link):
|
||||
super().__init__(link, "Link ID is invalid!", "invalidID")
|
||||
|
||||
class LinkNotSupported(GenerationError):
|
||||
def __init__(self, link):
|
||||
super().__init__(link, "Link is not supported.", "unsupportedURL")
|
||||
|
||||
class LinkNotRecognized(GenerationError):
|
||||
def __init__(self, link):
|
||||
super().__init__(link, "Link is not recognized.", "invalidURL")
|
||||
|
||||
class DownloadError(DeemixError):
|
||||
"""Download related errors"""
|
||||
|
||||
ErrorMessages = {
|
||||
'notOnDeezer': "Track not available on Deezer!",
|
||||
'notEncoded': "Track not yet encoded!",
|
||||
'notEncodedNoAlternative': "Track not yet encoded and no alternative found!",
|
||||
'wrongBitrate': "Track not found at desired bitrate.",
|
||||
'wrongBitrateNoAlternative': "Track not found at desired bitrate and no alternative found!",
|
||||
'wrongLicense': "Your account can't stream the track at the desired bitrate.",
|
||||
'no360RA': "Track is not available in Reality Audio 360.",
|
||||
'notAvailable': "Track not available on deezer's servers!",
|
||||
'notAvailableNoAlternative': "Track not available on deezer's servers and no alternative found!",
|
||||
'noSpaceLeft': "No space left on target drive, clean up some space for the tracks",
|
||||
'albumDoesntExists': "Track's album does not exsist, failed to gather info.",
|
||||
'notLoggedIn': "You need to login to download tracks.",
|
||||
'wrongGeolocation': "Your account can't stream the track from your current country.",
|
||||
'wrongGeolocationNoAlternative': "Your account can't stream the track from your current country and no alternative found."
|
||||
}
|
||||
|
||||
class DownloadFailed(DownloadError):
|
||||
def __init__(self, errid, track=None):
|
||||
super().__init__()
|
||||
self.errid = errid
|
||||
self.message = ErrorMessages[self.errid]
|
||||
self.track = track
|
||||
|
||||
class PreferredBitrateNotFound(DownloadError):
|
||||
pass
|
||||
|
||||
class TrackNot360(DownloadError):
|
||||
pass
|
||||
|
||||
class DownloadCanceled(DownloadError):
|
||||
pass
|
||||
|
||||
class DownloadEmpty(DownloadError):
|
||||
pass
|
||||
|
||||
class TrackError(DeemixError):
|
||||
"""Track generation related errors"""
|
||||
|
||||
class AlbumDoesntExists(TrackError):
|
||||
pass
|
||||
|
||||
class MD5NotFound(TrackError):
|
||||
pass
|
||||
|
||||
class NoDataToParse(TrackError):
|
||||
pass
|
|
@ -1,47 +1,52 @@
|
|||
import logging
|
||||
|
||||
from deezer.errors import GWAPIError, APIError
|
||||
from deezer.utils import map_user_playlist, map_track, map_album
|
||||
|
||||
from deemix.types.DownloadObjects import Single, Collection
|
||||
from deezer.gw import GWAPIError, LyricsStatus
|
||||
from deezer.api import APIError
|
||||
from deezer.utils import map_user_playlist
|
||||
from deemix.errors import GenerationError, ISRCnotOnDeezer, InvalidID, NotYourPrivatePlaylist
|
||||
|
||||
logger = logging.getLogger('deemix')
|
||||
|
||||
def generateTrackItem(dz, link_id, bitrate, trackAPI=None, albumAPI=None):
|
||||
# Check if is an isrc: url
|
||||
if str(link_id).startswith("isrc"):
|
||||
try:
|
||||
trackAPI = dz.api.get_track(link_id)
|
||||
except APIError as e:
|
||||
raise GenerationError(f"https://deezer.com/track/{link_id}", str(e)) from e
|
||||
|
||||
if 'id' in trackAPI and 'title' in trackAPI:
|
||||
link_id = trackAPI['id']
|
||||
else:
|
||||
raise ISRCnotOnDeezer(f"https://deezer.com/track/{link_id}")
|
||||
if not link_id.isdecimal(): raise InvalidID(f"https://deezer.com/track/{link_id}")
|
||||
|
||||
# Get essential track info
|
||||
try:
|
||||
trackAPI_gw = dz.gw.get_track_with_fallback(link_id)
|
||||
except GWAPIError as e:
|
||||
raise GenerationError(f"https://deezer.com/track/{link_id}", str(e)) from e
|
||||
if not trackAPI:
|
||||
if str(link_id).startswith("isrc") or int(link_id) > 0:
|
||||
try:
|
||||
trackAPI = dz.api.get_track(link_id)
|
||||
except APIError as e:
|
||||
raise GenerationError(f"https://deezer.com/track/{link_id}", str(e)) from e
|
||||
|
||||
title = trackAPI_gw['SNG_TITLE'].strip()
|
||||
if trackAPI_gw.get('VERSION') and trackAPI_gw['VERSION'] not in trackAPI_gw['SNG_TITLE']:
|
||||
title += f" {trackAPI_gw['VERSION']}".strip()
|
||||
explicit = bool(int(trackAPI_gw.get('EXPLICIT_LYRICS', 0)))
|
||||
# Check if is an isrc: url
|
||||
if str(link_id).startswith("isrc"):
|
||||
if 'id' in trackAPI and 'title' in trackAPI:
|
||||
link_id = trackAPI['id']
|
||||
else:
|
||||
raise ISRCnotOnDeezer(f"https://deezer.com/track/{link_id}")
|
||||
else:
|
||||
trackAPI_gw = dz.gw.get_track(link_id)
|
||||
trackAPI = map_track(trackAPI_gw)
|
||||
else:
|
||||
link_id = trackAPI['id']
|
||||
if not str(link_id).strip('-').isdecimal(): raise InvalidID(f"https://deezer.com/track/{link_id}")
|
||||
|
||||
cover = None
|
||||
if trackAPI['album']['cover_small']:
|
||||
cover = trackAPI['album']['cover_small'][:-24] + '/75x75-000000-80-0-0.jpg'
|
||||
else:
|
||||
cover = f"https://e-cdns-images.dzcdn.net/images/cover/{trackAPI['md5_image']}/75x75-000000-80-0-0.jpg"
|
||||
|
||||
if 'track_token' in trackAPI: del trackAPI['track_token']
|
||||
|
||||
return Single({
|
||||
'type': 'track',
|
||||
'id': link_id,
|
||||
'bitrate': bitrate,
|
||||
'title': title,
|
||||
'artist': trackAPI_gw['ART_NAME'],
|
||||
'cover': f"https://e-cdns-images.dzcdn.net/images/cover/{trackAPI_gw['ALB_PICTURE']}/75x75-000000-80-0-0.jpg",
|
||||
'explicit': explicit,
|
||||
'title': trackAPI['title'],
|
||||
'artist': trackAPI['artist']['name'],
|
||||
'cover': cover,
|
||||
'explicit': trackAPI['explicit_lyrics'],
|
||||
'single': {
|
||||
'trackAPI_gw': trackAPI_gw,
|
||||
'trackAPI': trackAPI,
|
||||
'albumAPI': albumAPI
|
||||
}
|
||||
|
@ -64,18 +69,25 @@ def generateAlbumItem(dz, link_id, bitrate, rootArtist=None):
|
|||
link_id = albumAPI['id']
|
||||
else:
|
||||
try:
|
||||
albumAPI = dz.api.get_album(link_id)
|
||||
albumAPI_gw_page = dz.gw.get_album_page(link_id)
|
||||
if 'DATA' in albumAPI_gw_page:
|
||||
albumAPI = map_album(albumAPI_gw_page['DATA'])
|
||||
link_id = albumAPI_gw_page['DATA']['ALB_ID']
|
||||
albumAPI_new = dz.api.get_album(link_id)
|
||||
albumAPI.update(albumAPI_new)
|
||||
else:
|
||||
raise GenerationError(f"https://deezer.com/album/{link_id}", "Can't find the album")
|
||||
except APIError as e:
|
||||
raise GenerationError(f"https://deezer.com/album/{link_id}", str(e)) from e
|
||||
|
||||
if not link_id.isdecimal(): raise InvalidID(f"https://deezer.com/album/{link_id}")
|
||||
if not str(link_id).isdecimal(): raise InvalidID(f"https://deezer.com/album/{link_id}")
|
||||
|
||||
# Get extra info about album
|
||||
# This saves extra api calls when downloading
|
||||
albumAPI_gw = dz.gw.get_album(link_id)
|
||||
albumAPI['nb_disk'] = albumAPI_gw['NUMBER_DISK']
|
||||
albumAPI['copyright'] = albumAPI_gw['COPYRIGHT']
|
||||
albumAPI['release_date'] = albumAPI_gw['PHYSICAL_RELEASE_DATE']
|
||||
albumAPI_gw = map_album(albumAPI_gw)
|
||||
albumAPI_gw.update(albumAPI)
|
||||
albumAPI = albumAPI_gw
|
||||
albumAPI['root_artist'] = rootArtist
|
||||
|
||||
# If the album is a single download as a track
|
||||
|
@ -89,18 +101,17 @@ def generateAlbumItem(dz, link_id, bitrate, rootArtist=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"
|
||||
cover = f"https://e-cdns-images.dzcdn.net/images/cover/{albumAPI['md5_image']}/75x75-000000-80-0-0.jpg"
|
||||
|
||||
totalSize = len(tracksArray)
|
||||
albumAPI['nb_tracks'] = totalSize
|
||||
collection = []
|
||||
for pos, trackAPI in enumerate(tracksArray, start=1):
|
||||
trackAPI['POSITION'] = pos
|
||||
trackAPI['SIZE'] = totalSize
|
||||
trackAPI = map_track(trackAPI)
|
||||
if 'track_token' in trackAPI: del trackAPI['track_token']
|
||||
trackAPI['position'] = pos
|
||||
collection.append(trackAPI)
|
||||
|
||||
explicit = albumAPI_gw.get('EXPLICIT_ALBUM_CONTENT', {}).get('EXPLICIT_LYRICS_STATUS', LyricsStatus.UNKNOWN) in [LyricsStatus.EXPLICIT, LyricsStatus.PARTIALLY_EXPLICIT]
|
||||
|
||||
return Collection({
|
||||
'type': 'album',
|
||||
'id': link_id,
|
||||
|
@ -108,17 +119,17 @@ def generateAlbumItem(dz, link_id, bitrate, rootArtist=None):
|
|||
'title': albumAPI['title'],
|
||||
'artist': albumAPI['artist']['name'],
|
||||
'cover': cover,
|
||||
'explicit': explicit,
|
||||
'explicit': albumAPI['explicit_lyrics'],
|
||||
'size': totalSize,
|
||||
'collection': {
|
||||
'tracks_gw': collection,
|
||||
'tracks': collection,
|
||||
'albumAPI': albumAPI
|
||||
}
|
||||
})
|
||||
|
||||
def generatePlaylistItem(dz, link_id, bitrate, playlistAPI=None, playlistTracksAPI=None):
|
||||
if not playlistAPI:
|
||||
if not link_id.isdecimal(): raise InvalidID(f"https://deezer.com/playlist/{link_id}")
|
||||
if not str(link_id).isdecimal(): raise InvalidID(f"https://deezer.com/playlist/{link_id}")
|
||||
# Get essential playlist info
|
||||
try:
|
||||
playlistAPI = dz.api.get_playlist(link_id)
|
||||
|
@ -145,10 +156,11 @@ def generatePlaylistItem(dz, link_id, bitrate, playlistAPI=None, playlistTracksA
|
|||
playlistAPI['nb_tracks'] = totalSize
|
||||
collection = []
|
||||
for pos, trackAPI in enumerate(playlistTracksAPI, start=1):
|
||||
if trackAPI.get('EXPLICIT_TRACK_CONTENT', {}).get('EXPLICIT_LYRICS_STATUS', LyricsStatus.UNKNOWN) in [LyricsStatus.EXPLICIT, LyricsStatus.PARTIALLY_EXPLICIT]:
|
||||
trackAPI = map_track(trackAPI)
|
||||
if trackAPI['explicit_lyrics']:
|
||||
playlistAPI['explicit'] = True
|
||||
trackAPI['POSITION'] = pos
|
||||
trackAPI['SIZE'] = totalSize
|
||||
if 'track_token' in trackAPI: del trackAPI['track_token']
|
||||
trackAPI['position'] = pos
|
||||
collection.append(trackAPI)
|
||||
|
||||
if 'explicit' not in playlistAPI: playlistAPI['explicit'] = False
|
||||
|
@ -163,13 +175,13 @@ def generatePlaylistItem(dz, link_id, bitrate, playlistAPI=None, playlistTracksA
|
|||
'explicit': playlistAPI['explicit'],
|
||||
'size': totalSize,
|
||||
'collection': {
|
||||
'tracks_gw': collection,
|
||||
'tracks': collection,
|
||||
'playlistAPI': playlistAPI
|
||||
}
|
||||
})
|
||||
|
||||
def generateArtistItem(dz, link_id, bitrate, listener=None):
|
||||
if not link_id.isdecimal(): raise InvalidID(f"https://deezer.com/artist/{link_id}")
|
||||
if not str(link_id).isdecimal(): raise InvalidID(f"https://deezer.com/artist/{link_id}")
|
||||
# Get essential artist info
|
||||
try:
|
||||
artistAPI = dz.api.get_artist(link_id)
|
||||
|
@ -196,7 +208,7 @@ def generateArtistItem(dz, link_id, bitrate, listener=None):
|
|||
return albumList
|
||||
|
||||
def generateArtistDiscographyItem(dz, link_id, bitrate, listener=None):
|
||||
if not link_id.isdecimal(): raise InvalidID(f"https://deezer.com/artist/{link_id}/discography")
|
||||
if not str(link_id).isdecimal(): raise InvalidID(f"https://deezer.com/artist/{link_id}/discography")
|
||||
# Get essential artist info
|
||||
try:
|
||||
artistAPI = dz.api.get_artist(link_id)
|
||||
|
@ -224,7 +236,7 @@ def generateArtistDiscographyItem(dz, link_id, bitrate, listener=None):
|
|||
return albumList
|
||||
|
||||
def generateArtistTopItem(dz, link_id, bitrate):
|
||||
if not link_id.isdecimal(): raise InvalidID(f"https://deezer.com/artist/{link_id}/top_track")
|
||||
if not str(link_id).isdecimal(): raise InvalidID(f"https://deezer.com/artist/{link_id}/top_track")
|
||||
# Get essential artist info
|
||||
try:
|
||||
artistAPI = dz.api.get_artist(link_id)
|
||||
|
@ -263,45 +275,3 @@ def generateArtistTopItem(dz, link_id, bitrate):
|
|||
|
||||
artistTopTracksAPI_gw = dz.gw.get_artist_toptracks(link_id)
|
||||
return generatePlaylistItem(dz, playlistAPI['id'], bitrate, playlistAPI=playlistAPI, playlistTracksAPI=artistTopTracksAPI_gw)
|
||||
|
||||
class GenerationError(Exception):
|
||||
def __init__(self, link, message, errid=None):
|
||||
super().__init__()
|
||||
self.link = link
|
||||
self.message = message
|
||||
self.errid = errid
|
||||
|
||||
def toDict(self):
|
||||
return {
|
||||
'link': self.link,
|
||||
'error': self.message,
|
||||
'errid': self.errid
|
||||
}
|
||||
|
||||
class ISRCnotOnDeezer(GenerationError):
|
||||
def __init__(self, link):
|
||||
super().__init__(link, "Track ISRC is not available on deezer", "ISRCnotOnDeezer")
|
||||
|
||||
class NotYourPrivatePlaylist(GenerationError):
|
||||
def __init__(self, link):
|
||||
super().__init__(link, "You can't download others private playlists.", "notYourPrivatePlaylist")
|
||||
|
||||
class TrackNotOnDeezer(GenerationError):
|
||||
def __init__(self, link):
|
||||
super().__init__(link, "Track not found on deezer!", "trackNotOnDeezer")
|
||||
|
||||
class AlbumNotOnDeezer(GenerationError):
|
||||
def __init__(self, link):
|
||||
super().__init__(link, "Album not found on deezer!", "albumNotOnDeezer")
|
||||
|
||||
class InvalidID(GenerationError):
|
||||
def __init__(self, link):
|
||||
super().__init__(link, "Link ID is invalid!", "invalidID")
|
||||
|
||||
class LinkNotSupported(GenerationError):
|
||||
def __init__(self, link):
|
||||
super().__init__(link, "Link is not supported.", "unsupportedURL")
|
||||
|
||||
class LinkNotRecognized(GenerationError):
|
||||
def __init__(self, link):
|
||||
super().__init__(link, "Link is not recognized.", "invalidURL")
|
||||
|
|
|
@ -1,15 +1,19 @@
|
|||
from concurrent.futures import ThreadPoolExecutor
|
||||
import json
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
import re
|
||||
from urllib.request import urlopen
|
||||
from deezer.errors import DataException
|
||||
from deemix.plugins import Plugin
|
||||
from deemix.utils.localpaths import getConfigFolder
|
||||
from deemix.itemgen import generateTrackItem, generateAlbumItem, GenerationError, TrackNotOnDeezer, AlbumNotOnDeezer
|
||||
from deemix.types.DownloadObjects import Convertable
|
||||
from deemix.itemgen import generateTrackItem, generateAlbumItem
|
||||
from deemix.errors import GenerationError, TrackNotOnDeezer, AlbumNotOnDeezer
|
||||
from deemix.types.DownloadObjects import Convertable, Collection
|
||||
|
||||
import spotipy
|
||||
SpotifyClientCredentials = spotipy.oauth2.SpotifyClientCredentials
|
||||
CacheFileHandler = spotipy.cache_handler.CacheFileHandler
|
||||
|
||||
class Spotify(Plugin):
|
||||
def __init__(self, configFolder=None):
|
||||
|
@ -54,7 +58,7 @@ class Spotify(Plugin):
|
|||
|
||||
return (link, link_type, link_id)
|
||||
|
||||
def generateDownloadObject(self, dz, link, bitrate):
|
||||
def generateDownloadObject(self, dz, link, bitrate, listener):
|
||||
(link, link_type, link_id) = self.parseLink(link)
|
||||
|
||||
if link_type is None or link_id is None: return None
|
||||
|
@ -91,7 +95,10 @@ class Spotify(Plugin):
|
|||
cachedTrack['id'] = trackID
|
||||
cache['tracks'][link_id] = cachedTrack
|
||||
self.saveCache(cache)
|
||||
if cachedTrack['id'] != "0": return generateTrackItem(dz, cachedTrack['id'], bitrate)
|
||||
|
||||
if cachedTrack.get('id', "0") != "0":
|
||||
return generateTrackItem(dz, cachedTrack['id'], bitrate)
|
||||
|
||||
raise TrackNotOnDeezer(f"https://open.spotify.com/track/{link_id}")
|
||||
|
||||
def generateAlbumItem(self, dz, link_id, bitrate):
|
||||
|
@ -112,9 +119,9 @@ class Spotify(Plugin):
|
|||
spotifyPlaylist = self.sp.playlist(link_id)
|
||||
|
||||
playlistAPI = self._convertPlaylistStructure(spotifyPlaylist)
|
||||
playlistAPI.various_artist = dz.api.get_artist(5080) # Useful for save as compilation
|
||||
playlistAPI['various_artist'] = dz.api.get_artist(5080) # Useful for save as compilation
|
||||
|
||||
tracklistTemp = spotifyPlaylist.track.items
|
||||
tracklistTemp = spotifyPlaylist['tracks']['items']
|
||||
while spotifyPlaylist['tracks']['next']:
|
||||
spotifyPlaylist['tracks'] = self.sp.next(spotifyPlaylist['tracks'])
|
||||
tracklistTemp += spotifyPlaylist['tracks']['items']
|
||||
|
@ -137,7 +144,7 @@ class Spotify(Plugin):
|
|||
'explicit': playlistAPI['explicit'],
|
||||
'size': len(tracklist),
|
||||
'collection': {
|
||||
'tracks_gw': [],
|
||||
'tracks': [],
|
||||
'playlistAPI': playlistAPI
|
||||
},
|
||||
'plugin': 'spotify',
|
||||
|
@ -179,8 +186,10 @@ class Spotify(Plugin):
|
|||
}
|
||||
return cachedAlbum
|
||||
|
||||
def convertTrack(self, dz, downloadObject, track, pos, conversion, conversionNext, cache, listener):
|
||||
def convertTrack(self, dz, downloadObject, track, pos, conversion, cache, listener):
|
||||
if downloadObject.isCanceled: return
|
||||
trackAPI = None
|
||||
cachedTrack = None
|
||||
|
||||
if track['id'] in cache['tracks']:
|
||||
cachedTrack = cache['tracks'][track['id']]
|
||||
|
@ -193,7 +202,7 @@ class Spotify(Plugin):
|
|||
try:
|
||||
trackAPI = dz.api.get_track_by_ISRC(cachedTrack['isrc'])
|
||||
if 'id' not in trackAPI or 'title' not in trackAPI: trackAPI = None
|
||||
except GenerationError: pass
|
||||
except DataException: pass
|
||||
if self.settings['fallbackSearch'] and not trackAPI:
|
||||
if 'id' not in cachedTrack or cachedTrack['id'] == "0":
|
||||
trackID = dz.api.get_track_id_from_metadata(
|
||||
|
@ -205,46 +214,59 @@ class Spotify(Plugin):
|
|||
cachedTrack['id'] = trackID
|
||||
cache['tracks'][track['id']] = cachedTrack
|
||||
self.saveCache(cache)
|
||||
if cachedTrack['id'] != "0": trackAPI = dz.api.get_track(cachedTrack['id'])
|
||||
|
||||
if cachedTrack.get('id', "0") != "0":
|
||||
trackAPI = dz.api.get_track(cachedTrack['id'])
|
||||
|
||||
if not trackAPI:
|
||||
deezerTrack = {
|
||||
'SNG_ID': "0",
|
||||
'SNG_TITLE': track['name'],
|
||||
'DURATION': 0,
|
||||
'MD5_ORIGIN': 0,
|
||||
'MEDIA_VERSION': 0,
|
||||
'FILESIZE': 0,
|
||||
'ALB_TITLE': track['album']['name'],
|
||||
'ALB_PICTURE': "",
|
||||
'ART_ID': 0,
|
||||
'ART_NAME': track['artists'][0]['name']
|
||||
trackAPI = {
|
||||
'id': "0",
|
||||
'title': track['name'],
|
||||
'duration': 0,
|
||||
'md5_origin': 0,
|
||||
'media_version': 0,
|
||||
'filesizes': {},
|
||||
'album': {
|
||||
'title': track['album']['name'],
|
||||
'md5_image': ""
|
||||
},
|
||||
'artist': {
|
||||
'id': 0,
|
||||
'name': track['artists'][0]['name']
|
||||
}
|
||||
}
|
||||
else:
|
||||
deezerTrack = dz.gw.get_track_with_fallback(trackAPI['id'])
|
||||
deezerTrack['_EXTRA_TRACK'] = trackAPI
|
||||
deezerTrack['POSITION'] = pos+1
|
||||
trackAPI['position'] = pos+1
|
||||
|
||||
conversionNext += (1 / downloadObject.size) * 100
|
||||
if round(conversionNext) != conversion and round(conversionNext) % 2 == 0:
|
||||
conversion = round(conversionNext)
|
||||
if listener: listener.send("updateQueue", {'uuid': downloadObject.uuid, 'conversion': conversion})
|
||||
conversion['next'] += (1 / downloadObject.size) * 100
|
||||
if round(conversion['next']) != conversion['now'] and round(conversion['next']) % 2 == 0:
|
||||
conversion['now'] = round(conversion['next'])
|
||||
if listener: listener.send("updateQueue", {'uuid': downloadObject.uuid, 'conversion': conversion['now']})
|
||||
|
||||
return trackAPI
|
||||
|
||||
def convert(self, dz, downloadObject, settings, listener=None):
|
||||
cache = self.loadCache()
|
||||
|
||||
conversion = 0
|
||||
conversionNext = 0
|
||||
conversion = { 'now': 0, 'next': 0 }
|
||||
|
||||
collection = [None] * len(downloadObject.conversion_data)
|
||||
if listener: listener.send("startConversion", downloadObject.uuid)
|
||||
with ThreadPoolExecutor(settings['queueConcurrency']) as executor:
|
||||
for pos, track in enumerate(downloadObject.conversion_data, start=0):
|
||||
collection[pos] = executor.submit(self.convertTrack,
|
||||
dz, downloadObject,
|
||||
track, pos,
|
||||
conversion, conversionNext,
|
||||
conversion,
|
||||
cache, listener
|
||||
)
|
||||
).result()
|
||||
|
||||
downloadObject.collection['tracks'] = collection
|
||||
downloadObject.size = len(collection)
|
||||
downloadObject = Collection(downloadObject.toDict())
|
||||
if listener: listener.send("finishConversion", downloadObject.getSlimmedDict())
|
||||
|
||||
self.saveCache(cache)
|
||||
return downloadObject
|
||||
|
||||
@classmethod
|
||||
def _convertPlaylistStructure(cls, spotifyPlaylist):
|
||||
|
@ -273,6 +295,7 @@ class Spotify(Plugin):
|
|||
'picture_medium': cover or "https://e-cdns-images.dzcdn.net/images/cover/d41d8cd98f00b204e9800998ecf8427e/250x250-000000-80-0-0.jpg",
|
||||
'picture_big': cover or "https://e-cdns-images.dzcdn.net/images/cover/d41d8cd98f00b204e9800998ecf8427e/500x500-000000-80-0-0.jpg",
|
||||
'picture_xl': cover or "https://e-cdns-images.dzcdn.net/images/cover/d41d8cd98f00b204e9800998ecf8427e/1000x1000-000000-80-0-0.jpg",
|
||||
'picture_thumbnail': cover or "https://e-cdns-images.dzcdn.net/images/cover/d41d8cd98f00b204e9800998ecf8427e/75x75-000000-80-0-0.jpg",
|
||||
'public': spotifyPlaylist['public'],
|
||||
'share': spotifyPlaylist['external_urls']['spotify'],
|
||||
'title': spotifyPlaylist['name'],
|
||||
|
@ -282,19 +305,27 @@ class Spotify(Plugin):
|
|||
return deezerPlaylist
|
||||
|
||||
def loadSettings(self):
|
||||
if not (self.configFolder / 'settings.json').is_file():
|
||||
with open(self.configFolder / 'settings.json', 'w') as f:
|
||||
if not (self.configFolder / 'config.json').is_file():
|
||||
with open(self.configFolder / 'config.json', 'w', encoding="utf-8") as f:
|
||||
json.dump({**self.credentials, **self.settings}, f, indent=2)
|
||||
|
||||
with open(self.configFolder / 'settings.json', 'r') as settingsFile:
|
||||
settings = json.load(settingsFile)
|
||||
with open(self.configFolder / 'config.json', 'r', encoding="utf-8") as settingsFile:
|
||||
try:
|
||||
settings = json.load(settingsFile)
|
||||
except json.decoder.JSONDecodeError:
|
||||
with open(self.configFolder / 'config.json', 'w', encoding="utf-8") as f:
|
||||
json.dump({**self.credentials, **self.settings}, f, indent=2)
|
||||
settings = deepcopy({**self.credentials, **self.settings})
|
||||
except Exception:
|
||||
settings = deepcopy({**self.credentials, **self.settings})
|
||||
|
||||
self.setSettings(settings)
|
||||
self.checkCredentials()
|
||||
|
||||
def saveSettings(self, newSettings=None):
|
||||
if newSettings: self.setSettings(newSettings)
|
||||
self.checkCredentials()
|
||||
with open(self.configFolder / 'settings.json', 'w') as f:
|
||||
with open(self.configFolder / 'config.json', 'w', encoding="utf-8") as f:
|
||||
json.dump({**self.credentials, **self.settings}, f, indent=2)
|
||||
|
||||
def getSettings(self):
|
||||
|
@ -308,15 +339,21 @@ class Spotify(Plugin):
|
|||
self.settings = settings
|
||||
|
||||
def loadCache(self):
|
||||
cache = None
|
||||
if (self.configFolder / 'cache.json').is_file():
|
||||
with open(self.configFolder / 'cache.json', 'r') as f:
|
||||
cache = json.load(f)
|
||||
else:
|
||||
cache = {'tracks': {}, 'albums': {}}
|
||||
with open(self.configFolder / 'cache.json', 'r', encoding="utf-8") as f:
|
||||
try:
|
||||
cache = json.load(f)
|
||||
except json.decoder.JSONDecodeError:
|
||||
self.saveCache({'tracks': {}, 'albums': {}})
|
||||
cache = None
|
||||
except Exception:
|
||||
cache = None
|
||||
if not cache: cache = {'tracks': {}, 'albums': {}}
|
||||
return cache
|
||||
|
||||
def saveCache(self, newCache):
|
||||
with open(self.configFolder / 'cache.json', 'w') as spotifyCache:
|
||||
with open(self.configFolder / 'cache.json', 'w', encoding="utf-8") as spotifyCache:
|
||||
json.dump(newCache, spotifyCache)
|
||||
|
||||
def checkCredentials(self):
|
||||
|
@ -325,8 +362,10 @@ class Spotify(Plugin):
|
|||
return
|
||||
|
||||
try:
|
||||
cache_handler = CacheFileHandler(self.configFolder / ".auth-cache")
|
||||
client_credentials_manager = SpotifyClientCredentials(client_id=self.credentials['clientId'],
|
||||
client_secret=self.credentials['clientSecret'])
|
||||
client_secret=self.credentials['clientSecret'],
|
||||
cache_handler=cache_handler)
|
||||
self.sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
|
||||
self.sp.user_playlists('spotify')
|
||||
self.enabled = True
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import json
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from os import makedirs
|
||||
from deezer import TrackFormats
|
||||
|
@ -20,89 +21,93 @@ class FeaturesOption():
|
|||
MOVE_TITLE = "2" # Move to track title
|
||||
|
||||
DEFAULTS = {
|
||||
"downloadLocation": str(localpaths.getMusicFolder()),
|
||||
"tracknameTemplate": "%artist% - %title%",
|
||||
"albumTracknameTemplate": "%tracknumber% - %title%",
|
||||
"playlistTracknameTemplate": "%position% - %artist% - %title%",
|
||||
"createPlaylistFolder": True,
|
||||
"playlistNameTemplate": "%playlist%",
|
||||
"createArtistFolder": False,
|
||||
"artistNameTemplate": "%artist%",
|
||||
"createAlbumFolder": True,
|
||||
"albumNameTemplate": "%artist% - %album%",
|
||||
"albumTracknameTemplate": "%tracknumber% - %title%",
|
||||
"albumVariousArtists": True,
|
||||
"artistCasing": "nothing",
|
||||
"artistImageTemplate": "folder",
|
||||
"artistNameTemplate": "%artist%",
|
||||
"coverImageTemplate": "cover",
|
||||
"createAlbumFolder": True,
|
||||
"createArtistFolder": False,
|
||||
"createCDFolder": True,
|
||||
"createStructurePlaylist": False,
|
||||
"createM3U8File": False,
|
||||
"createPlaylistFolder": True,
|
||||
"createSingleFolder": False,
|
||||
"padTracks": True,
|
||||
"paddingSize": "0",
|
||||
"illegalCharacterReplacer": "_",
|
||||
"queueConcurrency": 3,
|
||||
"maxBitrate": str(TrackFormats.MP3_320),
|
||||
"fallbackBitrate": True,
|
||||
"createStructurePlaylist": False,
|
||||
"dateFormat": "Y-M-D",
|
||||
"downloadLocation": str(localpaths.getMusicFolder()),
|
||||
"embeddedArtworkPNG": False,
|
||||
"embeddedArtworkSize": 800,
|
||||
"executeCommand": "",
|
||||
"fallbackBitrate": False,
|
||||
"fallbackISRC": False,
|
||||
"fallbackSearch": False,
|
||||
"featuredToTitle": FeaturesOption.NO_CHANGE,
|
||||
"feelingLucky": False,
|
||||
"illegalCharacterReplacer": "_",
|
||||
"jpegImageQuality": 90,
|
||||
"localArtworkFormat": "jpg",
|
||||
"localArtworkSize": 1400,
|
||||
"logErrors": True,
|
||||
"logSearched": False,
|
||||
"maxBitrate": TrackFormats.MP3_320,
|
||||
"overwriteFile": OverwriteOption.DONT_OVERWRITE,
|
||||
"createM3U8File": False,
|
||||
"paddingSize": "0",
|
||||
"padTracks": True,
|
||||
"playlistFilenameTemplate": "playlist",
|
||||
"syncedLyrics": False,
|
||||
"embeddedArtworkSize": 800,
|
||||
"embeddedArtworkPNG": False,
|
||||
"localArtworkSize": 1400,
|
||||
"localArtworkFormat": "jpg",
|
||||
"saveArtwork": True,
|
||||
"coverImageTemplate": "cover",
|
||||
"saveArtworkArtist": False,
|
||||
"artistImageTemplate": "folder",
|
||||
"jpegImageQuality": 80,
|
||||
"dateFormat": "Y-M-D",
|
||||
"albumVariousArtists": True,
|
||||
"playlistNameTemplate": "%playlist%",
|
||||
"playlistTracknameTemplate": "%position% - %artist% - %title%",
|
||||
"queueConcurrency": 3,
|
||||
"removeAlbumVersion": False,
|
||||
"removeDuplicateArtists": False,
|
||||
"featuredToTitle": FeaturesOption.NO_CHANGE,
|
||||
"titleCasing": "nothing",
|
||||
"artistCasing": "nothing",
|
||||
"executeCommand": "",
|
||||
"removeDuplicateArtists": True,
|
||||
"saveArtwork": True,
|
||||
"saveArtworkArtist": False,
|
||||
"syncedLyrics": False,
|
||||
"tags": {
|
||||
"title": True,
|
||||
"artist": True,
|
||||
"album": True,
|
||||
"cover": True,
|
||||
"trackNumber": True,
|
||||
"trackTotal": False,
|
||||
"discNumber": True,
|
||||
"discTotal": False,
|
||||
"albumArtist": True,
|
||||
"genre": True,
|
||||
"year": True,
|
||||
"date": True,
|
||||
"explicit": False,
|
||||
"isrc": True,
|
||||
"length": True,
|
||||
"artist": True,
|
||||
"artists": True,
|
||||
"barcode": True,
|
||||
"bpm": True,
|
||||
"replayGain": False,
|
||||
"label": True,
|
||||
"lyrics": False,
|
||||
"syncedLyrics": False,
|
||||
"copyright": False,
|
||||
"composer": False,
|
||||
"copyright": False,
|
||||
"cover": True,
|
||||
"coverDescriptionUTF8": False,
|
||||
"date": True,
|
||||
"discNumber": True,
|
||||
"discTotal": False,
|
||||
"explicit": False,
|
||||
"genre": True,
|
||||
"involvedPeople": False,
|
||||
"source": False,
|
||||
"savePlaylistAsCompilation": False,
|
||||
"useNullSeparator": False,
|
||||
"saveID3v1": True,
|
||||
"isrc": True,
|
||||
"label": True,
|
||||
"length": True,
|
||||
"lyrics": False,
|
||||
"multiArtistSeparator": "default",
|
||||
"rating": False,
|
||||
"replayGain": False,
|
||||
"saveID3v1": True,
|
||||
"savePlaylistAsCompilation": False,
|
||||
"singleAlbumArtist": False,
|
||||
"coverDescriptionUTF8": False
|
||||
}
|
||||
"source": False,
|
||||
"syncedLyrics": False,
|
||||
"title": True,
|
||||
"trackNumber": True,
|
||||
"trackTotal": False,
|
||||
"useNullSeparator": False,
|
||||
"year": True,
|
||||
},
|
||||
"titleCasing": "nothing",
|
||||
"tracknameTemplate": "%artist% - %title%",
|
||||
}
|
||||
|
||||
def save(settings, configFolder=None):
|
||||
configFolder = Path(configFolder or localpaths.getConfigFolder())
|
||||
makedirs(configFolder, exist_ok=True) # Create config folder if it doesn't exsist
|
||||
|
||||
with open(configFolder / 'config.json', 'w') as configFile:
|
||||
with open(configFolder / 'config.json', 'w', encoding="utf-8") as configFile:
|
||||
json.dump(settings, configFile, indent=2)
|
||||
|
||||
def load(configFolder=None):
|
||||
|
@ -111,16 +116,26 @@ def load(configFolder=None):
|
|||
if not (configFolder / 'config.json').is_file(): save(DEFAULTS, configFolder) # Create config file if it doesn't exsist
|
||||
|
||||
# Read config file
|
||||
with open(configFolder / 'config.json', 'r') as configFile:
|
||||
settings = json.load(configFile)
|
||||
with open(configFolder / 'config.json', 'r', encoding="utf-8") as configFile:
|
||||
try:
|
||||
settings = json.load(configFile)
|
||||
except json.decoder.JSONDecodeError:
|
||||
save(DEFAULTS, configFolder)
|
||||
settings = deepcopy(DEFAULTS)
|
||||
except Exception:
|
||||
settings = deepcopy(DEFAULTS)
|
||||
|
||||
if check(settings) > 0: save(settings, configFolder) # Check the settings and save them if something changed
|
||||
if check(settings) > 0:
|
||||
try:
|
||||
save(settings, configFolder) # Check the settings and save them if something changed
|
||||
except:
|
||||
print(f"Error saving config file {configFile.name}, continuing without saving.")
|
||||
return settings
|
||||
|
||||
def check(settings):
|
||||
changes = 0
|
||||
for i_set in DEFAULTS:
|
||||
if not i_set in settings or not isinstance(settings[i_set], type(DEFAULTS[i_set])):
|
||||
if not i_set in settings or not type(settings[i_set] is type(DEFAULTS[i_set])):
|
||||
settings[i_set] = DEFAULTS[i_set]
|
||||
changes += 1
|
||||
for i_set in DEFAULTS['tags']:
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from mutagen.flac import FLAC, Picture
|
||||
from mutagen.id3 import ID3, ID3NoHeaderError, \
|
||||
TXXX, TIT2, TPE1, TALB, TPE2, TRCK, TPOS, TCON, TYER, TDAT, TLEN, TBPM, \
|
||||
TPUB, TSRC, USLT, SYLT, APIC, IPLS, TCOM, TCOP, TCMP, Encoding, PictureType
|
||||
TPUB, TSRC, USLT, SYLT, APIC, IPLS, TCOM, TCOP, TCMP, Encoding, PictureType, POPM
|
||||
|
||||
# Adds tags to a MP3 file
|
||||
def tagID3(path, track, save):
|
||||
|
@ -25,7 +25,8 @@ def tagID3(path, track, save):
|
|||
tag.add(TPE1(text=track.artistsString))
|
||||
# Tag ARTISTS is added to keep the multiartist support when using a non standard tagging method
|
||||
# https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html#artists
|
||||
tag.add(TXXX(desc="ARTISTS", text=track.artists))
|
||||
if save['artists']:
|
||||
tag.add(TXXX(desc="ARTISTS", text=track.artists))
|
||||
|
||||
if save['album']:
|
||||
tag.add(TALB(text=track.album.title))
|
||||
|
@ -58,7 +59,7 @@ def tagID3(path, track, save):
|
|||
tag.add(TDAT(text=str(track.date.day) + str(track.date.month)))
|
||||
if save['length']:
|
||||
tag.add(TLEN(text=str(int(track.duration)*1000)))
|
||||
if save['bpm']:
|
||||
if save['bpm'] and track.bpm:
|
||||
tag.add(TBPM(text=str(track.bpm)))
|
||||
if save['label']:
|
||||
tag.add(TPUB(text=track.album.label))
|
||||
|
@ -89,7 +90,7 @@ def tagID3(path, track, save):
|
|||
if len(involved_people) > 0 and save['involvedPeople']:
|
||||
tag.add(IPLS(people=involved_people))
|
||||
|
||||
if save['copyright']:
|
||||
if save['copyright'] and track.copyright:
|
||||
tag.add(TCOP(text=track.copyright))
|
||||
if save['savePlaylistAsCompilation'] and track.playlist or track.album.recordType == "compile":
|
||||
tag.add(TCMP(text="1"))
|
||||
|
@ -98,6 +99,15 @@ def tagID3(path, track, save):
|
|||
tag.add(TXXX(desc="SOURCE", text='Deezer'))
|
||||
tag.add(TXXX(desc="SOURCEID", text=str(track.id)))
|
||||
|
||||
if save['rating']:
|
||||
rank = round((int(track.rank) / 10000) * 2.55)
|
||||
if rank > 255 :
|
||||
rank = 255
|
||||
else:
|
||||
rank = round(rank, 0)
|
||||
|
||||
tag.add(POPM(rating=rank))
|
||||
|
||||
if save['cover'] and track.album.embeddedCoverPath:
|
||||
|
||||
descEncoding = Encoding.LATIN1
|
||||
|
@ -136,7 +146,8 @@ def tagFLAC(path, track, save):
|
|||
tag["ARTIST"] = track.artistsString
|
||||
# Tag ARTISTS is added to keep the multiartist support when using a non standard tagging method
|
||||
# https://picard-docs.musicbrainz.org/en/technical/tag_mapping.html#artists
|
||||
tag["ARTISTS"] = track.artists
|
||||
if save['artists']:
|
||||
tag["ARTISTS"] = track.artists
|
||||
|
||||
if save['album']:
|
||||
tag["ALBUM"] = track.album.title
|
||||
|
@ -168,7 +179,7 @@ def tagFLAC(path, track, save):
|
|||
|
||||
if save['length']:
|
||||
tag["LENGTH"] = str(int(track.duration)*1000)
|
||||
if save['bpm']:
|
||||
if save['bpm'] and track.bpm:
|
||||
tag["BPM"] = str(track.bpm)
|
||||
if save['label']:
|
||||
tag["PUBLISHER"] = track.album.label
|
||||
|
@ -190,7 +201,7 @@ def tagFLAC(path, track, save):
|
|||
elif role == 'musicpublisher' and save['involvedPeople']:
|
||||
tag["ORGANIZATION"] = track.contributors['musicpublisher']
|
||||
|
||||
if save['copyright']:
|
||||
if save['copyright'] and track.copyright:
|
||||
tag["COPYRIGHT"] = track.copyright
|
||||
if save['savePlaylistAsCompilation'] and track.playlist or track.album.recordType == "compile":
|
||||
tag["COMPILATION"] = "1"
|
||||
|
@ -199,6 +210,10 @@ def tagFLAC(path, track, save):
|
|||
tag["SOURCE"] = 'Deezer'
|
||||
tag["SOURCEID"] = str(track.id)
|
||||
|
||||
if save['rating']:
|
||||
rank = round((int(track.rank) / 10000))
|
||||
tag['RATING'] = str(rank)
|
||||
|
||||
if save['cover'] and track.album.embeddedCoverPath:
|
||||
image = Picture()
|
||||
image.type = PictureType.COVER_FRONT
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
from deezer.gw import LyricsStatus
|
||||
|
||||
from deemix.utils import removeDuplicateArtists, removeFeatures
|
||||
from deemix.types.Artist import Artist
|
||||
from deemix.types.Date import Date
|
||||
|
@ -30,7 +28,7 @@ class Album:
|
|||
self.rootArtist = None
|
||||
self.variousArtists = None
|
||||
|
||||
self.playlistId = None
|
||||
self.playlistID = None
|
||||
self.owner = None
|
||||
self.isPlaylist = False
|
||||
|
||||
|
@ -39,8 +37,9 @@ class Album:
|
|||
|
||||
# Getting artist image ID
|
||||
# ex: https://e-cdns-images.dzcdn.net/images/artist/f2bc007e9133c946ac3c3907ddc5d2ea/56x56-000000-80-0-0.jpg
|
||||
art_pic = albumAPI['artist']['picture_small']
|
||||
art_pic = art_pic[art_pic.find('artist/') + 7:-24]
|
||||
art_pic = albumAPI['artist'].get('picture_small')
|
||||
if art_pic: art_pic = art_pic[art_pic.find('artist/') + 7:-24]
|
||||
else: art_pic = ""
|
||||
self.mainArtist = Artist(
|
||||
albumAPI['artist']['id'],
|
||||
albumAPI['artist']['name'],
|
||||
|
@ -78,57 +77,36 @@ class Album:
|
|||
self.artist[artist['role']].append(artist['name'])
|
||||
|
||||
self.trackTotal = albumAPI['nb_tracks']
|
||||
self.recordType = albumAPI['record_type']
|
||||
self.recordType = albumAPI.get('record_type', self.recordType)
|
||||
|
||||
self.barcode = albumAPI.get('upc', self.barcode)
|
||||
self.label = albumAPI.get('label', self.label)
|
||||
self.explicit = bool(albumAPI.get('explicit_lyrics', False))
|
||||
if 'release_date' in albumAPI:
|
||||
self.date.day = albumAPI["release_date"][8:10]
|
||||
self.date.month = albumAPI["release_date"][5:7]
|
||||
self.date.year = albumAPI["release_date"][0:4]
|
||||
release_date = albumAPI.get('release_date')
|
||||
if 'physical_release_date' in albumAPI:
|
||||
release_date = albumAPI['physical_release_date']
|
||||
if release_date:
|
||||
self.date.day = release_date[8:10]
|
||||
self.date.month = release_date[5:7]
|
||||
self.date.year = release_date[0:4]
|
||||
self.date.fixDayMonth()
|
||||
|
||||
self.discTotal = albumAPI.get('nb_disk')
|
||||
self.copyright = albumAPI.get('copyright')
|
||||
self.discTotal = albumAPI.get('nb_disk', "1")
|
||||
self.copyright = albumAPI.get('copyright', "")
|
||||
|
||||
if self.pic.md5 == "":
|
||||
# Getting album cover MD5
|
||||
# ex: https://e-cdns-images.dzcdn.net/images/cover/2e018122cb56986277102d2041a592c8/56x56-000000-80-0-0.jpg
|
||||
alb_pic = albumAPI['cover_small']
|
||||
self.pic.md5 = alb_pic[alb_pic.find('cover/') + 6:-24]
|
||||
if not self.pic.md5 or self.pic.md5 == "":
|
||||
if albumAPI.get('md5_image'):
|
||||
self.pic.md5 = albumAPI['md5_image']
|
||||
elif albumAPI.get('cover_small'):
|
||||
# Getting album cover MD5
|
||||
# ex: https://e-cdns-images.dzcdn.net/images/cover/2e018122cb56986277102d2041a592c8/56x56-000000-80-0-0.jpg
|
||||
alb_pic = albumAPI['cover_small']
|
||||
self.pic.md5 = alb_pic[alb_pic.find('cover/') + 6:-24]
|
||||
|
||||
if albumAPI.get('genres') and len(albumAPI['genres'].get('data', [])) > 0:
|
||||
for genre in albumAPI['genres']['data']:
|
||||
self.genre.append(genre['name'])
|
||||
|
||||
def parseAlbumGW(self, albumAPI_gw):
|
||||
self.title = albumAPI_gw['ALB_TITLE']
|
||||
self.mainArtist = Artist(
|
||||
art_id = albumAPI_gw['ART_ID'],
|
||||
name = albumAPI_gw['ART_NAME'],
|
||||
role = "Main"
|
||||
)
|
||||
|
||||
self.artists = [albumAPI_gw['ART_NAME']]
|
||||
self.trackTotal = albumAPI_gw['NUMBER_TRACK']
|
||||
self.discTotal = albumAPI_gw['NUMBER_DISK']
|
||||
self.label = albumAPI_gw.get('LABEL_NAME', self.label)
|
||||
|
||||
explicitLyricsStatus = albumAPI_gw.get('EXPLICIT_ALBUM_CONTENT', {}).get('EXPLICIT_LYRICS_STATUS', LyricsStatus.UNKNOWN)
|
||||
self.explicit = explicitLyricsStatus in [LyricsStatus.EXPLICIT, LyricsStatus.PARTIALLY_EXPLICIT]
|
||||
|
||||
self.addExtraAlbumGWData(albumAPI_gw)
|
||||
|
||||
def addExtraAlbumGWData(self, albumAPI_gw):
|
||||
if self.pic.md5 == "":
|
||||
self.pic.md5 = albumAPI_gw['ALB_PICTURE']
|
||||
if 'PHYSICAL_RELEASE_DATE' in albumAPI_gw:
|
||||
self.date.day = albumAPI_gw["PHYSICAL_RELEASE_DATE"][8:10]
|
||||
self.date.month = albumAPI_gw["PHYSICAL_RELEASE_DATE"][5:7]
|
||||
self.date.year = albumAPI_gw["PHYSICAL_RELEASE_DATE"][0:4]
|
||||
self.date.fixDayMonth()
|
||||
|
||||
def makePlaylistCompilation(self, playlist):
|
||||
self.variousArtists = playlist.variousArtists
|
||||
self.mainArtist = playlist.mainArtist
|
||||
|
@ -143,7 +121,7 @@ class Album:
|
|||
self.explicit = playlist.explicit
|
||||
self.date = playlist.date
|
||||
self.discTotal = playlist.discTotal
|
||||
self.playlistId = playlist.playlistId
|
||||
self.playlistID = playlist.playlistID
|
||||
self.owner = playlist.owner
|
||||
self.pic = playlist.pic
|
||||
self.isPlaylist = True
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
from pathlib import Path
|
||||
|
||||
class IDownloadObject:
|
||||
"""DownloadObject Interface"""
|
||||
def __init__(self, obj):
|
||||
|
@ -14,6 +16,8 @@ class IDownloadObject:
|
|||
self.progress = obj.get('progress', 0)
|
||||
self.errors = obj.get('errors', [])
|
||||
self.files = obj.get('files', [])
|
||||
self.extrasPath = obj.get('extrasPath', "")
|
||||
if self.extrasPath: self.extrasPath = Path(self.extrasPath)
|
||||
self.progressNext = 0
|
||||
self.uuid = f"{self.type}_{self.id}_{self.bitrate}"
|
||||
self.isCanceled = False
|
||||
|
@ -35,6 +39,7 @@ class IDownloadObject:
|
|||
'progress': self.progress,
|
||||
'errors': self.errors,
|
||||
'files': self.files,
|
||||
'extrasPath': str(self.extrasPath),
|
||||
'__type__': self.__type__
|
||||
}
|
||||
|
||||
|
@ -65,7 +70,8 @@ class IDownloadObject:
|
|||
'artist': self.artist,
|
||||
'cover': self.cover,
|
||||
'explicit': self.explicit,
|
||||
'size': self.size
|
||||
'size': self.size,
|
||||
'extrasPath': str(self.extrasPath)
|
||||
}
|
||||
|
||||
def updateProgress(self, listener=None):
|
||||
|
|
|
@ -25,5 +25,5 @@ class StaticPicture:
|
|||
def __init__(self, url):
|
||||
self.staticURL = url
|
||||
|
||||
def getURL(self):
|
||||
def getURL(self, _, __):
|
||||
return self.staticURL
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
from time import sleep
|
||||
import re
|
||||
import requests
|
||||
from datetime import datetime
|
||||
|
||||
from deezer.gw import GWAPIError
|
||||
from deezer.api import APIError
|
||||
from deezer.utils import map_track, map_album
|
||||
from deezer.errors import APIError, GWAPIError
|
||||
from deemix.errors import NoDataToParse, AlbumDoesntExists
|
||||
|
||||
from deemix.utils import removeFeatures, andCommaConcat, removeDuplicateArtists, generateReplayGainString, changeCase
|
||||
|
||||
|
@ -23,8 +23,11 @@ class Track:
|
|||
self.title = name
|
||||
self.MD5 = ""
|
||||
self.mediaVersion = ""
|
||||
self.trackToken = ""
|
||||
self.trackTokenExpiration = 0
|
||||
self.duration = 0
|
||||
self.fallbackID = "0"
|
||||
self.albumsFallback = []
|
||||
self.filesizes = {}
|
||||
self.local = False
|
||||
self.mainArtist = None
|
||||
|
@ -41,6 +44,7 @@ class Track:
|
|||
self.explicit = False
|
||||
self.ISRC = ""
|
||||
self.replayGain = ""
|
||||
self.rank = 0
|
||||
self.playlist = None
|
||||
self.position = None
|
||||
self.searched = False
|
||||
|
@ -50,78 +54,57 @@ class Track:
|
|||
self.artistsString = ""
|
||||
self.mainArtistsString = ""
|
||||
self.featArtistsString = ""
|
||||
self.urls = {}
|
||||
|
||||
def parseEssentialData(self, trackAPI_gw, trackAPI=None):
|
||||
self.id = str(trackAPI_gw['SNG_ID'])
|
||||
self.duration = trackAPI_gw['DURATION']
|
||||
self.MD5 = trackAPI_gw.get('MD5_ORIGIN')
|
||||
if not self.MD5:
|
||||
if trackAPI and trackAPI.get('md5_origin'):
|
||||
self.MD5 = trackAPI['md5_origin']
|
||||
else:
|
||||
raise MD5NotFound
|
||||
self.mediaVersion = trackAPI_gw['MEDIA_VERSION']
|
||||
def parseEssentialData(self, trackAPI):
|
||||
self.id = str(trackAPI['id'])
|
||||
self.duration = trackAPI['duration']
|
||||
self.trackToken = trackAPI['track_token']
|
||||
self.trackTokenExpiration = trackAPI['track_token_expire']
|
||||
self.MD5 = trackAPI.get('md5_origin')
|
||||
self.mediaVersion = trackAPI['media_version']
|
||||
self.filesizes = trackAPI['filesizes']
|
||||
self.fallbackID = "0"
|
||||
if 'FALLBACK' in trackAPI_gw:
|
||||
self.fallbackID = trackAPI_gw['FALLBACK']['SNG_ID']
|
||||
if 'fallback_id' in trackAPI:
|
||||
self.fallbackID = trackAPI['fallback_id']
|
||||
self.local = int(self.id) < 0
|
||||
self.urls = {}
|
||||
|
||||
def retriveFilesizes(self, dz):
|
||||
guest_sid = dz.session.cookies.get('sid')
|
||||
try:
|
||||
site = requests.post(
|
||||
"https://api.deezer.com/1.0/gateway.php",
|
||||
params={
|
||||
'api_key': "4VCYIJUCDLOUELGD1V8WBVYBNVDYOXEWSLLZDONGBBDFVXTZJRXPR29JRLQFO6ZE",
|
||||
'sid': guest_sid,
|
||||
'input': '3',
|
||||
'output': '3',
|
||||
'method': 'song_getData'
|
||||
},
|
||||
timeout=30,
|
||||
json={'sng_id': self.id},
|
||||
headers=dz.http_headers
|
||||
)
|
||||
result_json = site.json()
|
||||
except:
|
||||
sleep(2)
|
||||
self.retriveFilesizes(dz)
|
||||
if len(result_json['error']):
|
||||
raise TrackError(result_json.dumps(result_json['error']))
|
||||
response = result_json.get("results", {})
|
||||
filesizes = {}
|
||||
for key, value in response.items():
|
||||
if key.startswith("FILESIZE_"):
|
||||
filesizes[key] = int(value)
|
||||
filesizes[key+"_TESTED"] = False
|
||||
self.filesizes = filesizes
|
||||
def parseData(self, dz, track_id=None, trackAPI=None, albumAPI=None, playlistAPI=None):
|
||||
if track_id and (not trackAPI or trackAPI and not trackAPI.get('track_token')):
|
||||
trackAPI_new = dz.gw.get_track_with_fallback(track_id)
|
||||
trackAPI_new = map_track(trackAPI_new)
|
||||
if not trackAPI: trackAPI = {}
|
||||
trackAPI_new.update(trackAPI)
|
||||
trackAPI = trackAPI_new
|
||||
elif not trackAPI: raise NoDataToParse
|
||||
|
||||
def parseData(self, dz, track_id=None, trackAPI_gw=None, trackAPI=None, albumAPI_gw=None, albumAPI=None, playlistAPI=None):
|
||||
if track_id and not trackAPI_gw: trackAPI_gw = dz.gw.get_track_with_fallback(track_id)
|
||||
elif not trackAPI_gw: raise NoDataToParse
|
||||
if not trackAPI:
|
||||
try: trackAPI = dz.api.get_track(trackAPI_gw['SNG_ID'])
|
||||
except APIError: trackAPI = None
|
||||
self.parseEssentialData(trackAPI)
|
||||
|
||||
self.parseEssentialData(trackAPI_gw, trackAPI)
|
||||
# only public api has bpm
|
||||
if not trackAPI.get('bpm') and not self.local:
|
||||
try:
|
||||
trackAPI_new = dz.api.get_track(trackAPI['id'])
|
||||
trackAPI_new['release_date'] = trackAPI['release_date']
|
||||
trackAPI.update(trackAPI_new)
|
||||
except APIError: pass
|
||||
|
||||
if self.local:
|
||||
self.parseLocalTrackData(trackAPI_gw)
|
||||
self.parseLocalTrackData(trackAPI)
|
||||
else:
|
||||
self.retriveFilesizes(dz)
|
||||
self.parseTrackGW(trackAPI_gw)
|
||||
self.parseTrack(trackAPI)
|
||||
|
||||
# Get Lyrics data
|
||||
if not "LYRICS" in trackAPI_gw and self.lyrics.id != "0":
|
||||
try: trackAPI_gw["LYRICS"] = dz.gw.get_track_lyrics(self.id)
|
||||
if not trackAPI.get("lyrics") and self.lyrics.id != "0":
|
||||
try: trackAPI["lyrics"] = dz.gw.get_track_lyrics(self.id)
|
||||
except GWAPIError: self.lyrics.id = "0"
|
||||
if self.lyrics.id != "0": self.lyrics.parseLyrics(trackAPI_gw["LYRICS"])
|
||||
if self.lyrics.id != "0": self.lyrics.parseLyrics(trackAPI["lyrics"])
|
||||
|
||||
# Parse Album Data
|
||||
self.album = Album(
|
||||
alb_id = trackAPI_gw['ALB_ID'],
|
||||
title = trackAPI_gw['ALB_TITLE'],
|
||||
pic_md5 = trackAPI_gw.get('ALB_PICTURE')
|
||||
alb_id = trackAPI['album']['id'],
|
||||
title = trackAPI['album']['title'],
|
||||
pic_md5 = trackAPI['album'].get('md5_origin')
|
||||
)
|
||||
|
||||
# Get album Data
|
||||
|
@ -130,28 +113,31 @@ class Track:
|
|||
except APIError: albumAPI = None
|
||||
|
||||
# Get album_gw Data
|
||||
if not albumAPI_gw:
|
||||
try: albumAPI_gw = dz.gw.get_album(self.album.id)
|
||||
except GWAPIError: albumAPI_gw = None
|
||||
# Only gw has disk number
|
||||
if not albumAPI or albumAPI and not albumAPI.get('nb_disk'):
|
||||
try:
|
||||
albumAPI_gw = dz.gw.get_album(self.album.id)
|
||||
albumAPI_gw = map_album(albumAPI_gw)
|
||||
except GWAPIError: albumAPI_gw = {}
|
||||
if not albumAPI: albumAPI = {}
|
||||
albumAPI_gw.update(albumAPI)
|
||||
albumAPI = albumAPI_gw
|
||||
|
||||
if albumAPI:
|
||||
self.album.parseAlbum(albumAPI)
|
||||
elif albumAPI_gw:
|
||||
self.album.parseAlbumGW(albumAPI_gw)
|
||||
# albumAPI_gw doesn't contain the artist cover
|
||||
# Getting artist image ID
|
||||
# ex: https://e-cdns-images.dzcdn.net/images/artist/f2bc007e9133c946ac3c3907ddc5d2ea/56x56-000000-80-0-0.jpg
|
||||
if not albumAPI: raise AlbumDoesntExists
|
||||
|
||||
self.album.parseAlbum(albumAPI)
|
||||
# albumAPI_gw doesn't contain the artist cover
|
||||
# Getting artist image ID
|
||||
# ex: https://e-cdns-images.dzcdn.net/images/artist/f2bc007e9133c946ac3c3907ddc5d2ea/56x56-000000-80-0-0.jpg
|
||||
if not self.album.mainArtist.pic.md5 or self.album.mainArtist.pic.md5 == "":
|
||||
artistAPI = dz.api.get_artist(self.album.mainArtist.id)
|
||||
self.album.mainArtist.pic.md5 = artistAPI['picture_small'][artistAPI['picture_small'].find('artist/') + 7:-24]
|
||||
else:
|
||||
raise AlbumDoesntExists
|
||||
|
||||
# Fill missing data
|
||||
if albumAPI_gw: self.album.addExtraAlbumGWData(albumAPI_gw)
|
||||
if self.album.date and not self.date: self.date = self.album.date
|
||||
if not self.album.discTotal: self.album.discTotal = albumAPI_gw.get('NUMBER_DISK', "1")
|
||||
if not self.copyright: self.copyright = albumAPI_gw['COPYRIGHT']
|
||||
self.parseTrack(trackAPI)
|
||||
if 'genres' in trackAPI:
|
||||
for genre in trackAPI['genres']:
|
||||
if genre not in self.album.genre: self.album.genre.append(genre)
|
||||
|
||||
# Remove unwanted charaters in track name
|
||||
# Example: track/127793
|
||||
|
@ -159,9 +145,9 @@ class Track:
|
|||
|
||||
# Make sure there is at least one artist
|
||||
if len(self.artist['Main']) == 0:
|
||||
self.artist['Main'] = [self.mainArtist['name']]
|
||||
self.artist['Main'] = [self.mainArtist.name]
|
||||
|
||||
self.position = trackAPI_gw.get('POSITION')
|
||||
self.position = trackAPI.get('position')
|
||||
|
||||
# Add playlist data if track is in a playlist
|
||||
if playlistAPI: self.playlist = Playlist(playlistAPI)
|
||||
|
@ -169,64 +155,54 @@ class Track:
|
|||
self.generateMainFeatStrings()
|
||||
return self
|
||||
|
||||
def parseLocalTrackData(self, trackAPI_gw):
|
||||
def parseLocalTrackData(self, trackAPI):
|
||||
# Local tracks has only the trackAPI_gw page and
|
||||
# contains only the tags provided by the file
|
||||
self.title = trackAPI_gw['SNG_TITLE']
|
||||
self.album = Album(title=trackAPI_gw['ALB_TITLE'])
|
||||
self.title = trackAPI['title']
|
||||
self.album = Album(title=trackAPI['album']['title'])
|
||||
self.album.pic = Picture(
|
||||
md5 = trackAPI_gw.get('ALB_PICTURE', ""),
|
||||
md5 = trackAPI.get('md5_image', ""),
|
||||
pic_type = "cover"
|
||||
)
|
||||
self.mainArtist = Artist(name=trackAPI_gw['ART_NAME'], role="Main")
|
||||
self.artists = [trackAPI_gw['ART_NAME']]
|
||||
self.mainArtist = Artist(name=trackAPI['artist']['name'], role="Main")
|
||||
self.artists = [trackAPI['artist']['name']]
|
||||
self.artist = {
|
||||
'Main': [trackAPI_gw['ART_NAME']]
|
||||
'Main': [trackAPI['artist']['name']]
|
||||
}
|
||||
self.album.artist = self.artist
|
||||
self.album.artists = self.artists
|
||||
self.album.date = self.date
|
||||
self.album.mainArtist = self.mainArtist
|
||||
|
||||
def parseTrackGW(self, trackAPI_gw):
|
||||
self.title = trackAPI_gw['SNG_TITLE'].strip()
|
||||
if trackAPI_gw.get('VERSION') and not trackAPI_gw['VERSION'].strip() in self.title:
|
||||
self.title += f" {trackAPI_gw['VERSION'].strip()}"
|
||||
|
||||
self.discNumber = trackAPI_gw.get('DISK_NUMBER')
|
||||
self.explicit = bool(int(trackAPI_gw.get('EXPLICIT_LYRICS', "0")))
|
||||
self.copyright = trackAPI_gw.get('COPYRIGHT')
|
||||
if 'GAIN' in trackAPI_gw: self.replayGain = generateReplayGainString(trackAPI_gw['GAIN'])
|
||||
self.ISRC = trackAPI_gw.get('ISRC')
|
||||
self.trackNumber = trackAPI_gw['TRACK_NUMBER']
|
||||
self.contributors = trackAPI_gw['SNG_CONTRIBUTORS']
|
||||
|
||||
self.lyrics = Lyrics(trackAPI_gw.get('LYRICS_ID', "0"))
|
||||
|
||||
self.mainArtist = Artist(
|
||||
art_id = trackAPI_gw['ART_ID'],
|
||||
name = trackAPI_gw['ART_NAME'],
|
||||
role = "Main",
|
||||
pic_md5 = trackAPI_gw.get('ART_PICTURE')
|
||||
)
|
||||
|
||||
if 'PHYSICAL_RELEASE_DATE' in trackAPI_gw:
|
||||
self.date.day = trackAPI_gw["PHYSICAL_RELEASE_DATE"][8:10]
|
||||
self.date.month = trackAPI_gw["PHYSICAL_RELEASE_DATE"][5:7]
|
||||
self.date.year = trackAPI_gw["PHYSICAL_RELEASE_DATE"][0:4]
|
||||
self.date.fixDayMonth()
|
||||
|
||||
def parseTrack(self, trackAPI):
|
||||
self.title = trackAPI['title']
|
||||
|
||||
self.discNumber = trackAPI.get('disk_number')
|
||||
self.explicit = trackAPI.get('explicit_lyrics', False)
|
||||
self.copyright = trackAPI.get('copyright')
|
||||
if 'gain' in trackAPI: self.replayGain = generateReplayGainString(trackAPI['gain'])
|
||||
self.ISRC = trackAPI.get('isrc')
|
||||
self.trackNumber = trackAPI['track_position']
|
||||
self.contributors = trackAPI.get('song_contributors')
|
||||
self.rank = trackAPI['rank']
|
||||
self.bpm = trackAPI['bpm']
|
||||
|
||||
if not self.replayGain and 'gain' in trackAPI:
|
||||
self.replayGain = generateReplayGainString(trackAPI['gain'])
|
||||
if not self.explicit:
|
||||
self.explicit = trackAPI['explicit_lyrics']
|
||||
if not self.discNumber:
|
||||
self.discNumber = trackAPI['disk_number']
|
||||
self.lyrics = Lyrics(trackAPI.get('lyrics_id', "0"))
|
||||
|
||||
for artist in trackAPI['contributors']:
|
||||
self.mainArtist = Artist(
|
||||
art_id = trackAPI['artist']['id'],
|
||||
name = trackAPI['artist']['name'],
|
||||
role = "Main",
|
||||
pic_md5 = trackAPI['artist'].get('md5_image')
|
||||
)
|
||||
|
||||
if trackAPI.get('physical_release_date'):
|
||||
self.date.day = trackAPI["physical_release_date"][8:10]
|
||||
self.date.month = trackAPI["physical_release_date"][5:7]
|
||||
self.date.year = trackAPI["physical_release_date"][0:4]
|
||||
self.date.fixDayMonth()
|
||||
|
||||
for artist in trackAPI.get('contributors', []):
|
||||
isVariousArtists = str(artist['id']) == VARIOUS_ARTISTS
|
||||
isMainArtist = artist['role'] == "Main"
|
||||
|
||||
|
@ -241,6 +217,11 @@ class Track:
|
|||
self.artist[artist['role']] = []
|
||||
self.artist[artist['role']].append(artist['name'])
|
||||
|
||||
if trackAPI.get('alternative_albums'):
|
||||
for album in trackAPI['alternative_albums']['data']:
|
||||
if 'RIGHTS' in album and album['RIGHTS'].get('STREAM_ADS_AVAILABLE') or album['RIGHTS'].get('STREAM_SUB_AVAILABLE'):
|
||||
self.albumsFallback.append(album['ALB_ID'])
|
||||
|
||||
def removeDuplicateArtists(self):
|
||||
(self.artist, self.artists) = removeDuplicateArtists(self.artist, self.artists)
|
||||
|
||||
|
@ -259,6 +240,14 @@ class Track:
|
|||
if 'Featured' in self.artist:
|
||||
self.featArtistsString = "feat. "+andCommaConcat(self.artist['Featured'])
|
||||
|
||||
def checkAndRenewTrackToken(self, dz):
|
||||
now = datetime.now()
|
||||
expiration = datetime.fromtimestamp(self.trackTokenExpiration)
|
||||
if now > expiration:
|
||||
newTrack = dz.gw.get_track_with_fallback(self.id)
|
||||
self.trackToken = newTrack['TRACK_TOKEN']
|
||||
self.trackTokenExpiration = newTrack['TRACK_TOKEN_EXPIRE']
|
||||
|
||||
def applySettings(self, settings):
|
||||
|
||||
# Check if should save the playlist as a compilation
|
||||
|
@ -331,15 +320,3 @@ class Track:
|
|||
self.artistsString = separator.join(self.artist['Main'])
|
||||
else:
|
||||
self.artistsString = separator.join(self.artists)
|
||||
|
||||
class TrackError(Exception):
|
||||
"""Base class for exceptions in this module."""
|
||||
|
||||
class AlbumDoesntExists(TrackError):
|
||||
pass
|
||||
|
||||
class MD5NotFound(TrackError):
|
||||
pass
|
||||
|
||||
class NoDataToParse(TrackError):
|
||||
pass
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import string
|
||||
import re
|
||||
from deezer import TrackFormats
|
||||
import os
|
||||
from deemix.errors import ErrorMessages
|
||||
|
||||
USER_AGENT_HEADER = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) " \
|
||||
"Chrome/79.0.3945.130 Safari/537.36"
|
||||
|
@ -33,18 +35,35 @@ def changeCase(txt, case_type):
|
|||
if case_type == "upper":
|
||||
return txt.upper()
|
||||
if case_type == "start":
|
||||
return string.capwords(txt)
|
||||
txt = txt.strip().split(" ")
|
||||
for i, word in enumerate(txt):
|
||||
if word[0] in ['(', '{', '[', "'", '"']:
|
||||
txt[i] = word[0] + word[1:].capitalize()
|
||||
else:
|
||||
txt[i] = word.capitalize()
|
||||
return " ".join(txt)
|
||||
if case_type == "sentence":
|
||||
return txt.capitalize()
|
||||
return str
|
||||
|
||||
def removeFeatures(title):
|
||||
clean = title
|
||||
if "(feat." in clean.lower():
|
||||
pos = clean.lower().find("(feat.")
|
||||
found = False
|
||||
pos = -1
|
||||
if re.search(r"[\s(]\(?\s?feat\.?\s", clean):
|
||||
pos = re.search(r"[\s(]\(?\s?feat\.?\s", clean).start(0)
|
||||
found = True
|
||||
if re.search(r"[\s(]\(?\s?ft\.?\s", clean):
|
||||
pos = re.search(r"[\s(]\(?\s?ft\.?\s", clean).start(0)
|
||||
found = True
|
||||
openBracket = clean[pos] == '(' or clean[pos+1] == '('
|
||||
otherBracket = clean.find('(', pos+2)
|
||||
if found:
|
||||
tempTrack = clean[:pos]
|
||||
if ")" in clean:
|
||||
tempTrack += clean[clean.find(")", pos + 1) + 1:]
|
||||
if ")" in clean and openBracket:
|
||||
tempTrack += clean[clean.find(")", pos+2) + 1:]
|
||||
if not openBracket and otherBracket != -1:
|
||||
tempTrack += f" {clean[otherBracket:]}"
|
||||
clean = tempTrack.strip()
|
||||
clean = ' '.join(clean.split())
|
||||
return clean
|
||||
|
@ -73,3 +92,59 @@ def removeDuplicateArtists(artist, artists):
|
|||
for role in artist.keys():
|
||||
artist[role] = uniqueArray(artist[role])
|
||||
return (artist, artists)
|
||||
|
||||
def formatListener(key, data=None):
|
||||
if key == "startAddingArtist":
|
||||
return f"Started gathering {data['name']}'s albums ({data['id']})"
|
||||
if key == "finishAddingArtist":
|
||||
return f"Finished gathering {data['name']}'s albums ({data['id']})"
|
||||
if key == "updateQueue":
|
||||
uuid = f"[{data['uuid']}]"
|
||||
if data.get('downloaded'):
|
||||
shortFilepath = data['downloadPath'][len(data['extrasPath']):]
|
||||
return f"{uuid} Completed download of {shortFilepath}"
|
||||
if data.get('failed'):
|
||||
return f"{uuid} {data['data']['artist']} - {data['data']['title']} :: {data['error']}"
|
||||
if data.get('progress'):
|
||||
return f"{uuid} Download at {data['progress']}%"
|
||||
if data.get('conversion'):
|
||||
return f"{uuid} Conversion at {data['conversion']}%"
|
||||
return uuid
|
||||
if key == "downloadInfo":
|
||||
message = data['state']
|
||||
if data['state'] == "getTags": message = "Getting tags."
|
||||
elif data['state'] == "gotTags": message = "Tags got."
|
||||
elif data['state'] == "getBitrate": message = "Getting download URL."
|
||||
elif data['state'] == "bitrateFallback": message = "Desired bitrate not found, falling back to lower bitrate."
|
||||
elif data['state'] == "searchFallback": message = "This track has been searched for, result might not be 100% exact."
|
||||
elif data['state'] == "gotBitrate": message = "Download URL got."
|
||||
elif data['state'] == "getAlbumArt": message = "Downloading album art."
|
||||
elif data['state'] == "gotAlbumArt": message = "Album art downloaded."
|
||||
elif data['state'] == "downloading":
|
||||
message = "Downloading track."
|
||||
if data['alreadyStarted']:
|
||||
message += f" Recovering download from {data['value']}."
|
||||
else:
|
||||
message += f" Downloading {data['value']} bytes."
|
||||
elif data['state'] == "downloaded": message = "Track downloaded."
|
||||
elif data['state'] == "alreadyDownloaded": message = "Track already downloaded."
|
||||
elif data['state'] == "tagging": message = "Tagging track."
|
||||
elif data['state'] == "tagged": message = "Track tagged."
|
||||
return f"[{data['uuid']}] {data['data']['artist']} - {data['data']['title']} :: {message}"
|
||||
if key == "downloadWarn":
|
||||
errorMessage = ErrorMessages[data['state']]
|
||||
solutionMessage = ""
|
||||
if data['solution'] == 'fallback': solutionMessage = "Using fallback id."
|
||||
if data['solution'] == 'search': solutionMessage = "Searching for alternative."
|
||||
return f"[{data['uuid']}] {data['data']['artist']} - {data['data']['title']} :: {errorMessage} {solutionMessage}"
|
||||
if key == "currentItemCancelled":
|
||||
return f"Current item cancelled ({data})"
|
||||
if key == "removedFromQueue":
|
||||
return f"[{data}] Removed from the queue"
|
||||
if key == "finishDownload":
|
||||
return f"[{data}] Finished downloading"
|
||||
if key == "startConversion":
|
||||
return f"[{data}] Started converting"
|
||||
if key == "finishConversion":
|
||||
return f"[{data['uuid']}] Finished converting"
|
||||
return ""
|
||||
|
|
|
@ -20,7 +20,7 @@ def generateBlowfishKey(trackId):
|
|||
bfKey = ""
|
||||
for i in range(16):
|
||||
bfKey += chr(ord(idMd5[i]) ^ ord(idMd5[i + 16]) ^ ord(SECRET[i]))
|
||||
return bfKey
|
||||
return str.encode(bfKey)
|
||||
|
||||
def decryptChunk(key, data):
|
||||
return Blowfish.new(key, Blowfish.MODE_CBC, b"\x00\x01\x02\x03\x04\x05\x06\x07").decrypt(data)
|
||||
|
|
|
@ -5,28 +5,40 @@ CLIENT_ID = "172365"
|
|||
CLIENT_SECRET = "fb0bec7ccc063dab0417eb7b0d847f34"
|
||||
|
||||
def getAccessToken(email, password):
|
||||
accessToken = None
|
||||
password = _md5(password)
|
||||
request_hash = _md5(''.join([CLIENT_ID, email, password, CLIENT_SECRET]))
|
||||
response = requests.get(
|
||||
'https://api.deezer.com/auth/token',
|
||||
params={
|
||||
'app_id': CLIENT_ID,
|
||||
'login': email,
|
||||
'password': password,
|
||||
'hash': request_hash
|
||||
},
|
||||
headers={"User-Agent": USER_AGENT_HEADER}
|
||||
).json()
|
||||
return response.get('access_token')
|
||||
try:
|
||||
response = requests.get(
|
||||
'https://api.deezer.com/auth/token',
|
||||
params={
|
||||
'app_id': CLIENT_ID,
|
||||
'login': email,
|
||||
'password': password,
|
||||
'hash': request_hash
|
||||
},
|
||||
headers={"User-Agent": USER_AGENT_HEADER}
|
||||
).json()
|
||||
accessToken = response.get('access_token')
|
||||
if accessToken == "undefined": accessToken = None
|
||||
except Exception:
|
||||
pass
|
||||
return accessToken
|
||||
|
||||
def getArtFromAccessToken(accessToken):
|
||||
def getArlFromAccessToken(accessToken):
|
||||
if not accessToken: return None
|
||||
arl = None
|
||||
session = requests.Session()
|
||||
session.get(
|
||||
"https://api.deezer.com/platform/generic/track/3135556",
|
||||
headers={"Authorization": f"Bearer {accessToken}", "User-Agent": USER_AGENT_HEADER}
|
||||
)
|
||||
response = session.get(
|
||||
'https://www.deezer.com/ajax/gw-light.php?method=user.getArl&input=3&api_version=1.0&api_token=null',
|
||||
headers={"User-Agent": USER_AGENT_HEADER}
|
||||
).json()
|
||||
return response.get('results')
|
||||
try:
|
||||
session.get(
|
||||
"https://api.deezer.com/platform/generic/track/3135556",
|
||||
headers={"Authorization": f"Bearer {accessToken}", "User-Agent": USER_AGENT_HEADER}
|
||||
)
|
||||
response = session.get(
|
||||
'https://www.deezer.com/ajax/gw-light.php?method=user.getArl&input=3&api_version=1.0&api_token=null',
|
||||
headers={"User-Agent": USER_AGENT_HEADER}
|
||||
).json()
|
||||
arl = response.get('results')
|
||||
except Exception:
|
||||
pass
|
||||
return arl
|
||||
|
|
|
@ -44,22 +44,27 @@ def getMusicFolder():
|
|||
musicdata = Path(os.getenv("XDG_MUSIC_DIR"))
|
||||
musicdata = checkPath(musicdata)
|
||||
if (homedata / '.config' / 'user-dirs.dirs').is_file() and musicdata == "":
|
||||
with open(homedata / '.config' / 'user-dirs.dirs', 'r') as f:
|
||||
with open(homedata / '.config' / 'user-dirs.dirs', 'r', encoding="utf-8") as f:
|
||||
userDirs = f.read()
|
||||
musicdata = re.search(r"XDG_MUSIC_DIR=\"(.*)\"", userDirs).group(1)
|
||||
musicdata = Path(os.path.expandvars(musicdata))
|
||||
musicdata = checkPath(musicdata)
|
||||
musicdata_search = re.search(r"XDG_MUSIC_DIR=\"(.*)\"", userDirs)
|
||||
if musicdata_search:
|
||||
musicdata = musicdata_search.group(1)
|
||||
musicdata = Path(os.path.expandvars(musicdata))
|
||||
musicdata = checkPath(musicdata)
|
||||
if os.name == 'nt' and musicdata == "":
|
||||
musicKeys = ['My Music', '{4BD8D571-6D19-48D3-BE97-422220080E43}']
|
||||
regData = os.popen(r'reg.exe query "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"').read().split('\r\n')
|
||||
for i, line in enumerate(regData):
|
||||
if line == "": continue
|
||||
if i == 1: continue
|
||||
line = line.split(' ')
|
||||
if line[1] in musicKeys:
|
||||
musicdata = Path(line[3])
|
||||
break
|
||||
musicdata = checkPath(musicdata)
|
||||
try:
|
||||
musicKeys = ['My Music', '{4BD8D571-6D19-48D3-BE97-422220080E43}']
|
||||
regData = os.popen(r'reg.exe query "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"').read().split('\r\n')
|
||||
for i, line in enumerate(regData):
|
||||
if line == "": continue
|
||||
if i == 1: continue
|
||||
line = line.split(' ')
|
||||
if line[1] in musicKeys:
|
||||
musicdata = Path(line[3])
|
||||
break
|
||||
musicdata = checkPath(musicdata)
|
||||
except Exception:
|
||||
musicdata = ""
|
||||
if musicdata == "":
|
||||
musicdata = homedata / 'Music'
|
||||
musicdata = checkPath(musicdata)
|
||||
|
|
|
@ -2,5 +2,5 @@ click
|
|||
pycryptodomex
|
||||
mutagen
|
||||
requests
|
||||
spotipy>=2.11.0
|
||||
deezer-py
|
||||
spotipy>=2.11.0
|
||||
|
|
7
setup.py
7
setup.py
|
@ -7,7 +7,7 @@ README = (HERE / "README.md").read_text()
|
|||
|
||||
setup(
|
||||
name="deemix",
|
||||
version="3.0.0",
|
||||
version="3.6.6",
|
||||
description="A barebone deezer downloader library",
|
||||
long_description=README,
|
||||
long_description_content_type="text/markdown",
|
||||
|
@ -23,7 +23,10 @@ setup(
|
|||
python_requires='>=3.7',
|
||||
packages=find_packages(exclude=("tests",)),
|
||||
include_package_data=True,
|
||||
install_requires=["click", "pycryptodomex", "mutagen", "requests", "spotipy>=2.11.0", "deezer-py"],
|
||||
install_requires=["click", "pycryptodomex", "mutagen", "requests", "deezer-py>=1.3.0"],
|
||||
extras_require={
|
||||
"spotify": ["spotipy>=2.11.0"]
|
||||
},
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
"deemix=deemix.__main__:download",
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
pkgs ? import <nixpkgs> { },
|
||||
}:
|
||||
|
||||
pkgs.mkShell {
|
||||
buildInputs = [
|
||||
pkgs.python312
|
||||
pkgs.python312Packages.virtualenv
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
if [ ! -d .venv ]; then
|
||||
virtualenv .venv
|
||||
. .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
pip install -e .
|
||||
else
|
||||
. .venv/bin/activate
|
||||
fi
|
||||
'';
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
rm -rd build
|
||||
rm -rd dist
|
||||
#python -m bump
|
||||
#python -m bump deemix/__init__.py
|
||||
python3 setup.py sdist bdist_wheel
|
||||
python3 -m twine upload dist/*
|
Loading…
Reference in New Issue