deemix-py/deemix/downloader.py

565 lines
26 KiB
Python
Raw Permalink Normal View History

2021-02-11 16:30:53 +00:00
from concurrent.futures import ThreadPoolExecutor
from time import sleep
from os.path import sep as pathSep
2021-04-10 09:53:52 +00:00
from os import makedirs, system as execute
from pathlib import Path
from shlex import quote
import errno
2020-08-15 19:34:10 +00:00
2021-04-10 09:53:52 +00:00
import logging
from tempfile import gettempdir
import requests
from requests import get
2020-11-30 13:55:38 +00:00
from urllib3.exceptions import SSLError as u3SSLError
2020-08-15 19:34:10 +00:00
2021-04-10 09:53:52 +00:00
from mutagen.flac import FLACNoHeaderError, error as FLACError
from deezer import TrackFormats
2021-03-19 13:31:32 +00:00
from deemix.types.DownloadObjects import Single, Collection
2021-06-07 18:25:51 +00:00
from deemix.types.Track import Track, AlbumDoesntExists, MD5NotFound
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
2021-03-19 13:31:32 +00:00
from deemix.settings import OverwriteOption
2020-08-15 19:34:10 +00:00
logger = logging.getLogger('deemix')
extensions = {
TrackFormats.FLAC: '.flac',
TrackFormats.LOCAL: '.mp3',
TrackFormats.MP3_320: '.mp3',
TrackFormats.MP3_128: '.mp3',
TrackFormats.DEFAULT: '.mp3',
TrackFormats.MP4_RA3: '.mp4',
TrackFormats.MP4_RA2: '.mp4',
TrackFormats.MP4_RA1: '.mp4'
2020-08-15 19:34:10 +00:00
}
2021-06-07 18:25:51 +00:00
TEMPDIR = Path(gettempdir()) / 'deemix-imgs'
if not TEMPDIR.is_dir(): makedirs(TEMPDIR)
def downloadImage(url, path, overwrite=OverwriteOption.DONT_OVERWRITE):
2021-06-07 18:25:51 +00:00
if path.is_file() and overwrite not in [OverwriteOption.OVERWRITE, OverwriteOption.ONLY_TAGS, OverwriteOption.KEEP_BOTH]: return path
try:
image = get(url, headers={'User-Agent': USER_AGENT_HEADER}, timeout=30)
image.raise_for_status()
with open(path, 'wb') as f:
f.write(image.content)
return path
except requests.exceptions.HTTPError:
if path.is_file(): path.unlink()
if 'cdns-images.dzcdn.net' in url:
urlBase = url[:url.rfind("/")+1]
pictureUrl = url[len(urlBase):]
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:
if path.is_file(): path.unlink()
sleep(5)
return downloadImage(url, path, overwrite)
except OSError as e:
if path.is_file(): path.unlink()
2021-06-07 18:25:51 +00:00
if e.errno == errno.ENOSPC: raise DownloadFailed("noSpaceLeft") from e
logger.exception("Error while downloading an image, you should report this to the developers: %s", e)
return None
2020-08-15 19:34:10 +00:00
2021-06-07 18:25:51 +00:00
def getPreferredBitrate(track, bitrate, shouldFallback, uuid=None, listener=None):
bitrate = int(bitrate)
if track.local: return TrackFormats.LOCAL
2021-03-19 13:31:32 +00:00
falledBack = False
formats_non_360 = {
TrackFormats.FLAC: "FLAC",
TrackFormats.MP3_320: "MP3_320",
TrackFormats.MP3_128: "MP3_128",
}
formats_360 = {
TrackFormats.MP4_RA3: "MP4_RA3",
TrackFormats.MP4_RA2: "MP4_RA2",
TrackFormats.MP4_RA1: "MP4_RA1",
}
2021-06-07 18:25:51 +00:00
is360format = bitrate in formats_360.keys()
2021-03-19 13:31:32 +00:00
if not shouldFallback:
formats = formats_360
formats.update(formats_non_360)
elif is360format:
formats = formats_360
else:
formats = formats_non_360
2021-06-07 18:25:51 +00:00
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()
2021-06-13 12:06:17 +00:00
track.filesizes[f"FILESIZE_{formatName}"] = int(request.headers["Content-Length"])
2021-06-07 18:25:51 +00:00
track.filesizes[f"FILESIZE_{formatName}_TESTED"] = True
2021-06-13 12:06:17 +00:00
if track.filesizes[f"FILESIZE_{formatName}"] == 0: return None
2021-06-07 18:25:51 +00:00
return formatNumber
except requests.exceptions.HTTPError: # if the format is not available, Deezer returns a 403 error
return None
2021-03-19 13:31:32 +00:00
for formatNumber, formatName in formats.items():
2021-06-08 17:39:56 +00:00
if formatNumber > bitrate: continue
2021-04-10 09:53:52 +00:00
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"]:
2021-06-07 18:25:51 +00:00
testedBitrate = testBitrate(track, formatNumber, formatName)
if testedBitrate: return testedBitrate
2021-04-10 09:53:52 +00:00
if not shouldFallback:
raise PreferredBitrateNotFound
if not falledBack:
falledBack = True
logger.info("%s Fallback to lower bitrate", f"[{track.mainArtist.name} - {track.title}]")
2021-06-07 18:25:51 +00:00
if listener and uuid:
listener.send('queueUpdate', {
'uuid': uuid,
2021-04-10 09:53:52 +00:00
'bitrateFallback': True,
'data': {
'id': track.id,
'title': track.title,
'artist': track.mainArtist.name
},
})
2021-03-19 13:31:32 +00:00
if is360format: raise TrackNot360
return TrackFormats.DEFAULT
2020-08-15 19:34:10 +00:00
2021-03-19 13:31:32 +00:00
class Downloader:
2021-06-07 18:25:51 +00:00
def __init__(self, dz, downloadObject, settings, listener=None):
2020-08-15 19:34:10 +00:00
self.dz = dz
2021-03-19 13:31:32 +00:00
self.downloadObject = downloadObject
self.settings = settings
self.bitrate = downloadObject.bitrate
2021-06-07 18:25:51 +00:00
self.listener = listener
2020-08-17 15:46:21 +00:00
self.extrasPath = None
self.playlistCoverName = None
2020-08-15 21:36:32 +00:00
self.playlistURLs = []
2020-08-15 19:34:10 +00:00
def start(self):
if not self.downloadObject.isCanceled:
if isinstance(self.downloadObject, Single):
track = self.downloadWrapper({
'trackAPI_gw': self.downloadObject.single['trackAPI_gw'],
2021-06-08 17:12:15 +00:00
'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'])
with ThreadPoolExecutor(self.settings['queueConcurrency']) as executor:
for pos, track in enumerate(self.downloadObject.collection['tracks_gw'], start=0):
tracks[pos] = executor.submit(self.downloadWrapper, {
'trackAPI_gw': track,
2021-06-08 17:12:15 +00:00
'albumAPI': self.downloadObject.collection.get('albumAPI'),
'playlistAPI': self.downloadObject.collection.get('playlistAPI')
})
self.afterDownloadCollection(tracks)
if self.listener:
2021-06-07 18:25:51 +00:00
if self.listener:
self.listener.send('currentItemCancelled', self.downloadObject.uuid)
self.listener.send("removedFromQueue", self.downloadObject.uuid)
else:
self.listener.send("finishDownload", self.downloadObject.uuid)
2021-06-07 18:25:51 +00:00
def download(self, extraData, track=None):
returnData = {}
trackAPI_gw = extraData['trackAPI_gw']
2021-06-08 17:12:15 +00:00
trackAPI = extraData.get('trackAPI')
albumAPI = extraData.get('albumAPI')
playlistAPI = extraData.get('playlistAPI')
2021-06-07 18:25:51 +00:00
if self.downloadObject.isCanceled: raise DownloadCanceled
2020-10-02 16:57:23 +00:00
if trackAPI_gw['SNG_ID'] == "0": raise DownloadFailed("notOnDeezer")
2020-08-15 19:34:10 +00:00
2021-04-10 09:53:52 +00:00
itemName = f"[{trackAPI_gw['ART_NAME']} - {trackAPI_gw['SNG_TITLE']}]"
2020-08-15 19:34:10 +00:00
# Create Track object
if not track:
2021-04-10 09:53:52 +00:00
logger.info("%s Getting the tags", itemName)
2020-09-24 17:20:01 +00:00
try:
2021-01-31 16:59:15 +00:00
track = Track().parseData(
dz=self.dz,
trackAPI_gw=trackAPI_gw,
2021-03-19 13:31:32 +00:00
trackAPI=trackAPI,
albumAPI=albumAPI,
playlistAPI=playlistAPI
2021-01-31 16:59:15 +00:00
)
2021-04-10 09:53:52 +00:00
except AlbumDoesntExists as e:
raise DownloadError('albumDoesntExists') from e
2021-06-07 18:25:51 +00:00
except MD5NotFound as e:
raise DownloadError('notLoggedIn') from e
2021-04-10 09:53:52 +00:00
itemName = f"[{track.mainArtist.name} - {track.title}]"
2020-08-15 19:34:10 +00:00
2020-10-02 16:57:23 +00:00
# Check if track not yet encoded
2021-03-19 13:31:32 +00:00
if track.MD5 == '': raise DownloadFailed("notEncoded", track)
2020-08-15 19:34:10 +00:00
# Choose the target bitrate
2020-10-02 16:57:23 +00:00
try:
2021-03-19 13:31:32 +00:00
selectedFormat = getPreferredBitrate(
track,
self.bitrate,
self.settings['fallbackBitrate'],
2021-06-07 18:25:51 +00:00
self.downloadObject.uuid, self.listener
2021-03-19 13:31:32 +00:00
)
2021-04-10 09:53:52 +00:00
except PreferredBitrateNotFound as e:
raise DownloadFailed("wrongBitrate", track) from e
except TrackNot360 as e:
raise DownloadFailed("no360RA") from e
2021-06-07 18:25:51 +00:00
track.bitrate = selectedFormat
2021-01-31 16:59:15 +00:00
track.album.bitrate = selectedFormat
2020-08-15 19:34:10 +00:00
2021-06-07 18:25:51 +00:00
# Apply settings
track.applySettings(self.settings)
2020-08-15 19:34:10 +00:00
# Generate filename and filepath from metadata
2021-06-07 18:25:51 +00:00
(filename, filepath, artistPath, coverPath, extrasPath) = generatePath(track, self.downloadObject, self.settings)
2021-03-19 13:31:32 +00:00
# Make sure the filepath exists
makedirs(filepath, exist_ok=True)
2021-06-07 18:25:51 +00:00
extension = extensions[track.bitrate]
writepath = filepath / f"{filename}{extension}"
2021-03-19 13:31:32 +00:00
# Save extrasPath
2021-06-07 18:25:51 +00:00
if extrasPath and not self.extrasPath: self.extrasPath = extrasPath
# Generate covers URLs
embeddedImageFormat = f'jpg-{self.settings["jpegImageQuality"]}'
if self.settings['embeddedArtworkPNG']: embeddedImageFormat = 'png'
track.album.embeddedCoverURL = track.album.pic.getURL(self.settings['embeddedArtworkSize'], embeddedImageFormat)
ext = track.album.embeddedCoverURL[-4:]
if ext[0] != ".": ext = ".jpg" # Check for Spotify images
track.album.embeddedCoverPath = TEMPDIR / ((f"pl{track.playlist.id}" if track.album.isPlaylist else f"alb{track.album.id}") + f"_{self.settings['embeddedArtworkSize']}{ext}")
2020-08-15 19:34:10 +00:00
# Download and cache coverart
2021-04-10 09:53:52 +00:00
logger.info("%s Getting the album cover", itemName)
2021-06-16 13:27:22 +00:00
track.album.embeddedCoverPath = downloadImage(track.album.embeddedCoverURL, track.album.embeddedCoverPath)
2020-08-15 19:34:10 +00:00
# Save local album art
if coverPath:
2021-06-07 18:25:51 +00:00
returnData['albumURLs'] = []
2021-04-10 09:53:52 +00:00
for pic_format in self.settings['localArtworkFormat'].split(","):
if pic_format in ["png","jpg"]:
extendedFormat = pic_format
if extendedFormat == "jpg": extendedFormat += f"-{self.settings['jpegImageQuality']}"
2021-06-07 18:25:51 +00:00
url = track.album.pic.getURL(self.settings['localArtworkSize'], extendedFormat)
# Skip non deezer pictures at the wrong format
if isinstance(track.album.pic, StaticPicture) and pic_format != "jpg":
2021-04-10 09:53:52 +00:00
continue
2021-06-07 18:25:51 +00:00
returnData['albumURLs'].append({'url': url, 'ext': pic_format})
returnData['albumPath'] = coverPath
returnData['albumFilename'] = generateAlbumName(self.settings['coverImageTemplate'], track.album, self.settings, track.playlist)
2020-08-15 19:34:10 +00:00
# Save artist art
if artistPath:
2021-06-07 18:25:51 +00:00
returnData['artistURLs'] = []
2021-04-10 09:53:52 +00:00
for pic_format in self.settings['localArtworkFormat'].split(","):
2021-06-07 18:25:51 +00:00
# Deezer doesn't support png artist images
if pic_format == "jpg":
extendedFormat = f"{pic_format}-{self.settings['jpegImageQuality']}"
url = track.album.mainArtist.pic.getURL(self.settings['localArtworkSize'], extendedFormat)
if track.album.mainArtist.pic.md5 == "": continue
returnData['artistURLs'].append({'url': url, 'ext': pic_format})
returnData['artistPath'] = artistPath
returnData['artistFilename'] = generateArtistName(self.settings['artistImageTemplate'], track.album.mainArtist, self.settings, rootArtist=track.album.rootArtist)
2020-08-15 19:34:10 +00:00
2021-03-19 13:31:32 +00:00
# Save playlist art
if track.playlist:
2021-06-07 18:25:51 +00:00
if len(self.playlistURLs) == 0:
2021-04-10 09:53:52 +00:00
for pic_format in self.settings['localArtworkFormat'].split(","):
if pic_format in ["png","jpg"]:
extendedFormat = pic_format
if extendedFormat == "jpg": extendedFormat += f"-{self.settings['jpegImageQuality']}"
2021-06-07 18:25:51 +00:00
url = track.playlist.pic.getURL(self.settings['localArtworkSize'], extendedFormat)
if isinstance(track.playlist.pic, StaticPicture) and pic_format != "jpg": continue
2021-04-10 09:53:52 +00:00
self.playlistURLs.append({'url': url, 'ext': pic_format})
if not self.playlistCoverName:
2021-01-31 16:59:15 +00:00
track.playlist.bitrate = selectedFormat
track.playlist.dateString = track.playlist.date.format(self.settings['dateFormat'])
2021-06-07 18:25:51 +00:00
self.playlistCoverName = generateAlbumName(self.settings['coverImageTemplate'], track.playlist, self.settings, track.playlist)
2020-08-15 19:34:10 +00:00
# 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'))
2020-08-15 19:34:10 +00:00
2021-03-19 13:31:32 +00:00
# Check for overwrite settings
trackAlreadyDownloaded = writepath.is_file()
2020-10-02 16:57:23 +00:00
# Don't overwrite and don't mind extension
if not trackAlreadyDownloaded and self.settings['overwriteFile'] == OverwriteOption.DONT_CHECK_EXT:
exts = ['.mp3', '.flac', '.opus', '.m4a']
baseFilename = str(filepath / filename)
for ext in exts:
trackAlreadyDownloaded = Path(baseFilename+ext).is_file()
2020-10-02 16:57:23 +00:00
if trackAlreadyDownloaded: break
# Don't overwrite and keep both files
if trackAlreadyDownloaded and self.settings['overwriteFile'] == OverwriteOption.KEEP_BOTH:
baseFilename = str(filepath / filename)
2021-06-07 18:25:51 +00:00
c = 1
currentFilename = baseFilename+' ('+str(c)+')'+ extension
while Path(currentFilename).is_file():
2021-06-07 18:25:51 +00:00
c += 1
currentFilename = baseFilename+' ('+str(c)+')'+ extension
2020-08-15 19:34:10 +00:00
trackAlreadyDownloaded = False
writepath = Path(currentFilename)
2020-08-15 19:34:10 +00:00
if not trackAlreadyDownloaded or self.settings['overwriteFile'] == OverwriteOption.OVERWRITE:
2021-04-10 09:53:52 +00:00
logger.info("%s Downloading the track", itemName)
2021-06-07 18:25:51 +00:00
track.downloadUrl = generateStreamURL(track.id, track.MD5, track.mediaVersion, track.bitrate)
2020-09-18 20:17:58 +00:00
try:
2021-06-07 18:25:51 +00:00
with open(writepath, 'wb') as stream:
streamTrack(stream, track, downloadObject=self.downloadObject, listener=self.listener)
2021-06-08 17:12:15 +00:00
except requests.exceptions.HTTPError as e:
raise DownloadFailed('notAvailable', track) from e
2021-06-07 18:25:51 +00:00
except OSError as e:
if writepath.is_file(): writepath.unlink()
if e.errno == errno.ENOSPC: raise DownloadFailed("noSpaceLeft") from e
2020-09-18 20:17:58 +00:00
raise e
2020-08-15 19:34:10 +00:00
2020-09-18 20:17:58 +00:00
else:
2021-04-10 09:53:52 +00:00
logger.info("%s Skipping track as it's already downloaded", itemName)
2021-06-07 18:25:51 +00:00
self.downloadObject.completeTrackProgress(self.listener)
2020-09-18 20:17:58 +00:00
# Adding tags
2021-06-07 18:25:51 +00:00
if (not trackAlreadyDownloaded or self.settings['overwriteFile'] in [OverwriteOption.ONLY_TAGS, OverwriteOption.OVERWRITE]) and not track.local:
2021-04-10 09:53:52 +00:00
logger.info("%s Applying tags to the track", itemName)
2021-06-07 18:25:51 +00:00
if extension == '.mp3':
2020-09-18 20:17:58 +00:00
tagID3(writepath, track, self.settings['tags'])
2021-06-07 18:25:51 +00:00
elif extension == '.flac':
2020-09-18 20:17:58 +00:00
try:
tagFLAC(writepath, track, self.settings['tags'])
2020-09-30 07:33:14 +00:00
except (FLACNoHeaderError, FLACError):
2021-06-07 18:25:51 +00:00
writepath.unlink()
2021-04-10 09:53:52 +00:00
logger.warning("%s Track not available in FLAC, falling back if necessary", itemName)
2021-06-07 18:25:51 +00:00
self.downloadObject.removeTrackProgress(self.listener)
2020-09-18 20:17:58 +00:00
track.filesizes['FILESIZE_FLAC'] = "0"
track.filesizes['FILESIZE_FLAC_TESTED'] = True
2021-03-19 13:31:32 +00:00
return self.download(trackAPI_gw, track=track)
2020-09-18 20:17:58 +00:00
2021-06-07 18:25:51 +00:00
if track.searched: returnData['searched'] = True
2021-03-19 13:31:32 +00:00
self.downloadObject.downloaded += 1
self.downloadObject.files.append(str(writepath))
self.downloadObject.extrasPath = str(self.extrasPath)
2021-06-07 18:25:51 +00:00
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)
})
returnData['filename'] = str(writepath)[len(str(extrasPath))+ len(pathSep):]
returnData['data'] = {
'id': track.id,
'title': track.title,
'artist': track.mainArtist.name
}
return returnData
2020-08-15 19:34:10 +00:00
2021-06-07 18:25:51 +00:00
def downloadWrapper(self, extraData, track=None):
trackAPI_gw = extraData['trackAPI_gw']
2021-06-08 17:11:46 +00:00
if ('_EXTRA_TRACK' in trackAPI_gw):
extraData['trackAPI'] = trackAPI_gw['_EXTRA_TRACK'].copy()
del extraData['trackAPI_gw']['_EXTRA_TRACK']
del trackAPI_gw['_EXTRA_TRACK']
2021-03-19 13:31:32 +00:00
# Temp metadata to generate logs
tempTrack = {
2020-08-15 21:03:05 +00:00
'id': trackAPI_gw['SNG_ID'],
2020-10-02 16:57:23 +00:00
'title': trackAPI_gw['SNG_TITLE'].strip(),
2020-08-15 21:03:05 +00:00
'artist': trackAPI_gw['ART_NAME']
2020-08-15 19:34:10 +00:00
}
2020-10-02 16:57:23 +00:00
if trackAPI_gw.get('VERSION') and trackAPI_gw['VERSION'] not in trackAPI_gw['SNG_TITLE']:
2021-03-19 13:31:32 +00:00
tempTrack['title'] += f" {trackAPI_gw['VERSION']}".strip()
2020-08-15 19:34:10 +00:00
2021-06-08 17:12:15 +00:00
itemName = f"[{tempTrack['artist']} - {tempTrack['title']}]"
2021-04-10 09:53:52 +00:00
2020-08-15 19:34:10 +00:00
try:
2021-06-07 18:25:51 +00:00
result = self.download(extraData, track)
2020-08-15 19:34:10 +00:00
except DownloadFailed as error:
2021-03-19 13:31:32 +00:00
if error.track:
track = error.track
2021-03-24 16:41:03 +00:00
if track.fallbackID != "0":
2021-04-10 09:53:52 +00:00
logger.warning("%s %s Using fallback id", itemName, error.message)
2021-03-24 16:41:03 +00:00
newTrack = self.dz.gw.get_track_with_fallback(track.fallbackID)
2021-03-19 13:31:32 +00:00
track.parseEssentialData(newTrack)
track.retriveFilesizes(self.dz)
2021-06-07 18:25:51 +00:00
return self.downloadWrapper(extraData, track)
2021-04-10 09:53:52 +00:00
if not track.searched and self.settings['fallbackSearch']:
logger.warning("%s %s Searching for alternative", itemName, error.message)
2021-03-19 13:31:32 +00:00
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)
track.parseEssentialData(newTrack)
track.retriveFilesizes(self.dz)
track.searched = True
2021-06-07 18:25:51 +00:00
if self.listener: self.listener.send('queueUpdate', {
'uuid': self.downloadObject.uuid,
'searchFallback': True,
'data': {
'id': track.id,
'title': track.title,
'artist': track.mainArtist.name
},
})
return self.downloadWrapper(extraData, track)
error.errid += "NoAlternative"
error.message = errorMessages[error.errid]
2021-04-10 09:53:52 +00:00
logger.error("%s %s", itemName, error.message)
2020-08-15 19:34:10 +00:00
result = {'error': {
2021-04-10 09:53:52 +00:00
'message': error.message,
'errid': error.errid,
'data': tempTrack
}}
2020-08-15 19:34:10 +00:00
except Exception as e:
2021-04-10 09:53:52 +00:00
logger.exception("%s %s", itemName, e)
2020-08-15 19:34:10 +00:00
result = {'error': {
2021-06-07 18:25:51 +00:00
'message': str(e),
'data': tempTrack
}}
2020-08-15 19:34:10 +00:00
if 'error' in result:
2021-06-07 18:25:51 +00:00
self.downloadObject.completeTrackProgress(self.listener)
2021-03-19 13:31:32 +00:00
self.downloadObject.failed += 1
self.downloadObject.errors.append(result['error'])
2021-06-07 18:25:51 +00:00
if self.listener:
2020-08-15 19:34:10 +00:00
error = result['error']
2021-06-07 18:25:51 +00:00
self.listener.send("updateQueue", {
2021-03-19 13:31:32 +00:00
'uuid': self.downloadObject.uuid,
2020-08-15 19:34:10 +00:00
'failed': True,
'data': error['data'],
'error': error['message'],
'errid': error['errid'] if 'errid' in error else None
})
return result
2021-06-07 18:25:51 +00:00
def afterDownloadSingle(self, track):
2021-03-19 13:31:32 +00:00
if not self.extrasPath: self.extrasPath = Path(self.settings['downloadLocation'])
# Save Album Cover
2021-06-07 18:25:51 +00:00
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'])
2021-03-19 13:31:32 +00:00
# Save Artist Artwork
2021-06-07 18:25:51 +00:00
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'])
2021-03-19 13:31:32 +00:00
# Create searched logfile
2021-06-07 18:25:51 +00:00
if self.settings['logSearched'] and 'searched' in track:
filename = f"{track.data.artist} - {track.data.title}"
2021-03-19 13:31:32 +00:00
with open(self.extrasPath / 'searched.txt', 'wb+') as f:
2021-06-07 18:25:51 +00:00
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'))
2021-03-19 13:31:32 +00:00
# Execute command after download
if self.settings['executeCommand'] != "":
2021-06-07 18:25:51 +00:00
execute(self.settings['executeCommand'].replace("%folder%", quote(str(self.extrasPath))).replace("%filename%", quote(track['filename'])), shell=True)
2021-03-19 13:31:32 +00:00
2021-06-07 18:25:51 +00:00
def afterDownloadCollection(self, tracks):
2021-03-19 13:31:32 +00:00
if not self.extrasPath: self.extrasPath = Path(self.settings['downloadLocation'])
playlist = [None] * len(tracks)
errors = ""
searched = ""
2021-06-07 18:25:51 +00:00
for i, track in enumerate(tracks):
track = track.result()
if not track: return # Check if item is cancelled
2021-03-19 13:31:32 +00:00
# Log errors to file
2021-06-07 18:25:51 +00:00
if track.get('error'):
if not track['error'].get('data'): track['error']['data'] = {'id': "0", 'title': 'Unknown', 'artist': 'Unknown'}
errors += f"{track['error']['data']['id']} | {track['error']['data']['artist']} - {track['error']['data']['title']} | {track['error']['message']}\r\n"
2021-03-19 13:31:32 +00:00
# Log searched to file
2021-06-07 18:25:51 +00:00
if 'searched' in track: searched += track['searched'] + "\r\n"
2021-03-19 13:31:32 +00:00
# Save Album Cover
2021-06-07 18:25:51 +00:00
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'])
2021-03-19 13:31:32 +00:00
# Save Artist Artwork
2021-06-07 18:25:51 +00:00
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'])
2021-03-19 13:31:32 +00:00
# Save filename for playlist file
2021-06-07 18:25:51 +00:00
playlist[i] = track.get('filename', "")
2021-03-19 13:31:32 +00:00
# Create errors logfile
if self.settings['logErrors'] and errors != "":
with open(self.extrasPath / 'errors.txt', 'wb') as f:
f.write(errors.encode('utf-8'))
# Create searched logfile
if self.settings['logSearched'] and searched != "":
with open(self.extrasPath / 'searched.txt', 'wb') as f:
f.write(searched.encode('utf-8'))
# 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'])
# Create M3U8 File
if self.settings['createM3U8File']:
2021-06-07 18:25:51 +00:00
filename = generateDownloadObjectName(self.settings['playlistFilenameTemplate'], self.downloadObject, self.settings) or "playlist"
2021-03-19 13:31:32 +00:00
with open(self.extrasPath / f'{filename}.m3u8', 'wb') as f:
for line in playlist:
f.write((line + "\n").encode('utf-8'))
# Execute command after download
if self.settings['executeCommand'] != "":
execute(self.settings['executeCommand'].replace("%folder%", quote(str(self.extrasPath))), shell=True)
2020-08-15 19:34:10 +00:00
class DownloadError(Exception):
"""Base class for exceptions in this module."""
2021-06-07 18:25:51 +00:00
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"
}
2020-08-15 19:34:10 +00:00
class DownloadFailed(DownloadError):
2021-03-19 13:31:32 +00:00
def __init__(self, errid, track=None):
2021-04-10 09:53:52 +00:00
super().__init__()
2020-08-15 19:34:10 +00:00
self.errid = errid
self.message = errorMessages[self.errid]
2021-03-19 13:31:32 +00:00
self.track = track
2020-08-15 19:34:10 +00:00
2020-10-02 16:57:23 +00:00
class PreferredBitrateNotFound(DownloadError):
pass
class TrackNot360(DownloadError):
pass