2021-06-27 20:29:41 +00:00
|
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
|
|
from time import sleep
|
2021-09-21 06:57:16 +00:00
|
|
|
import traceback
|
2021-06-27 20:29:41 +00:00
|
|
|
|
|
|
|
from os.path import sep as pathSep
|
|
|
|
from os import makedirs, system as execute
|
|
|
|
from pathlib import Path
|
|
|
|
from shlex import quote
|
|
|
|
import errno
|
|
|
|
|
|
|
|
import logging
|
|
|
|
from tempfile import gettempdir
|
|
|
|
|
|
|
|
import requests
|
|
|
|
from requests import get
|
|
|
|
|
|
|
|
from urllib3.exceptions import SSLError as u3SSLError
|
|
|
|
|
|
|
|
from mutagen.flac import FLACNoHeaderError, error as FLACError
|
|
|
|
|
|
|
|
from deezer import TrackFormats
|
2021-08-02 20:42:03 +00:00
|
|
|
from deezer.errors import WrongLicense, WrongGeolocation
|
2021-06-27 20:29:41 +00:00
|
|
|
from deemix.types.DownloadObjects import Single, Collection
|
2021-08-02 19:55:22 +00:00
|
|
|
from deemix.types.Track import Track
|
2021-06-27 20:29:41 +00:00
|
|
|
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
|
2021-08-02 19:55:22 +00:00
|
|
|
from deemix.decryption import generateCryptedStreamURL, streamTrack
|
2021-06-27 20:29:41 +00:00
|
|
|
from deemix.settings import OverwriteOption
|
2021-08-02 19:55:22 +00:00
|
|
|
from deemix.errors import DownloadFailed, MD5NotFound, DownloadCanceled, PreferredBitrateNotFound, TrackNot360, AlbumDoesntExists, DownloadError, ErrorMessages
|
2021-06-27 20:29:41 +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'
|
|
|
|
}
|
|
|
|
|
2021-07-25 09:33:59 +00:00
|
|
|
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'
|
|
|
|
}
|
|
|
|
|
2021-06-27 20:29:41 +00:00
|
|
|
TEMPDIR = Path(gettempdir()) / 'deemix-imgs'
|
|
|
|
if not TEMPDIR.is_dir(): makedirs(TEMPDIR)
|
|
|
|
|
|
|
|
def downloadImage(url, path, overwrite=OverwriteOption.DONT_OVERWRITE):
|
|
|
|
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()
|
|
|
|
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
|
|
|
|
|
2021-08-02 20:42:03 +00:00
|
|
|
def getPreferredBitrate(dz, track, preferredBitrate, shouldFallback, uuid=None, listener=None):
|
|
|
|
preferredBitrate = int(preferredBitrate)
|
2021-06-27 20:29:41 +00:00
|
|
|
|
|
|
|
falledBack = False
|
2021-08-02 20:42:03 +00:00
|
|
|
hasAlternative = track.fallbackID != "0"
|
|
|
|
isGeolocked = False
|
|
|
|
wrongLicense = False
|
2021-06-27 20:29:41 +00:00
|
|
|
|
2021-08-02 20:42:03 +00:00
|
|
|
def testURL(track, url, formatName):
|
2021-08-03 16:53:15 +00:00
|
|
|
if not url: return False
|
2021-06-27 20:29:41 +00:00
|
|
|
request = requests.head(
|
2021-08-02 20:42:03 +00:00
|
|
|
url,
|
2021-06-27 20:29:41 +00:00
|
|
|
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
|
2021-08-02 20:42:03 +00:00
|
|
|
return track.filesizes[f"FILESIZE_{formatName}"] != 0
|
2021-06-27 20:29:41 +00:00
|
|
|
except requests.exceptions.HTTPError: # if the format is not available, Deezer returns a 403 error
|
2021-08-02 20:42:03 +00:00
|
|
|
return False
|
|
|
|
|
|
|
|
def getCorrectURL(track, formatName, formatNumber):
|
|
|
|
nonlocal wrongLicense, isGeolocked
|
2021-08-04 19:36:34 +00:00
|
|
|
url = None
|
2021-08-02 20:42:03 +00:00
|
|
|
# Check the track with the legit method
|
|
|
|
try:
|
|
|
|
url = dz.get_track_url(track.trackToken, formatName)
|
|
|
|
if testURL(track, url, formatName): return url
|
|
|
|
url = None
|
|
|
|
except (WrongLicense, WrongGeolocation) as e:
|
|
|
|
wrongLicense = isinstance(e, WrongLicense)
|
|
|
|
isGeolocked = isinstance(e, WrongGeolocation)
|
|
|
|
# Fallback to old method
|
|
|
|
if not url:
|
|
|
|
url = generateCryptedStreamURL(track.id, track.MD5, track.mediaVersion, formatNumber)
|
|
|
|
if testURL(track, url, formatName): return url
|
|
|
|
url = None
|
|
|
|
return url
|
2021-06-27 20:29:41 +00:00
|
|
|
|
2021-09-21 12:33:54 +00:00
|
|
|
if track.local:
|
|
|
|
url = getCorrectURL(track, "MP3_MISC", TrackFormats.LOCAL)
|
|
|
|
track.urls["MP3_MISC"] = url
|
|
|
|
return TrackFormats.LOCAL
|
|
|
|
|
|
|
|
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",
|
|
|
|
}
|
|
|
|
|
|
|
|
is360format = preferredBitrate in formats_360.keys()
|
|
|
|
if not shouldFallback:
|
|
|
|
formats = formats_360
|
|
|
|
formats.update(formats_non_360)
|
|
|
|
elif is360format:
|
|
|
|
formats = formats_360
|
|
|
|
else:
|
|
|
|
formats = formats_non_360
|
|
|
|
|
2021-06-27 20:29:41 +00:00
|
|
|
for formatNumber, formatName in formats.items():
|
2021-08-02 20:42:03 +00:00
|
|
|
# Current bitrate is higher than preferred bitrate; skip
|
|
|
|
if formatNumber > preferredBitrate: continue
|
|
|
|
|
|
|
|
currentTrack = track
|
|
|
|
url = getCorrectURL(currentTrack, formatName, formatNumber)
|
|
|
|
newTrack = None
|
|
|
|
while True:
|
|
|
|
if not url and hasAlternative:
|
|
|
|
newTrack = dz.gw.get_track_with_fallback(currentTrack.fallbackID)
|
|
|
|
currentTrack = Track()
|
|
|
|
currentTrack.parseEssentialData(newTrack)
|
|
|
|
hasAlternative = currentTrack.fallbackID != "0"
|
|
|
|
if not url: getCorrectURL(currentTrack, formatName, formatNumber)
|
|
|
|
if (url or not hasAlternative): break
|
|
|
|
|
|
|
|
if url:
|
|
|
|
if newTrack: track.parseEssentialData(newTrack)
|
|
|
|
track.urls[formatName] = url
|
|
|
|
return formatNumber
|
2021-06-27 20:29:41 +00:00
|
|
|
|
|
|
|
if not shouldFallback:
|
2021-08-02 20:42:03 +00:00
|
|
|
if wrongLicense: raise WrongLicense(formatName)
|
|
|
|
if isGeolocked: raise WrongGeolocation(dz.current_user['country'])
|
2021-06-27 20:29:41 +00:00
|
|
|
raise PreferredBitrateNotFound
|
|
|
|
if not falledBack:
|
|
|
|
falledBack = True
|
|
|
|
logger.info("%s Fallback to lower bitrate", f"[{track.mainArtist.name} - {track.title}]")
|
|
|
|
if listener and uuid:
|
2021-08-02 21:45:08 +00:00
|
|
|
listener.send('downloadInfo', {
|
2021-06-27 20:29:41 +00:00
|
|
|
'uuid': uuid,
|
2021-08-02 21:45:08 +00:00
|
|
|
'state': 'bitrateFallback',
|
2021-06-27 20:29:41 +00:00
|
|
|
'data': {
|
|
|
|
'id': track.id,
|
|
|
|
'title': track.title,
|
|
|
|
'artist': track.mainArtist.name
|
|
|
|
},
|
|
|
|
})
|
|
|
|
if is360format: raise TrackNot360
|
2021-09-21 12:33:54 +00:00
|
|
|
url = getCorrectURL(track, "MP3_MISC", TrackFormats.DEFAULT)
|
|
|
|
track.urls["MP3_MISC"] = url
|
2021-06-27 20:29:41 +00:00
|
|
|
return TrackFormats.DEFAULT
|
|
|
|
|
|
|
|
class Downloader:
|
|
|
|
def __init__(self, dz, downloadObject, settings, listener=None):
|
|
|
|
self.dz = dz
|
|
|
|
self.downloadObject = downloadObject
|
|
|
|
self.settings = settings
|
|
|
|
self.bitrate = downloadObject.bitrate
|
|
|
|
self.listener = listener
|
|
|
|
|
|
|
|
self.playlistCoverName = None
|
|
|
|
self.playlistURLs = []
|
|
|
|
|
|
|
|
def start(self):
|
|
|
|
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'])
|
|
|
|
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,
|
|
|
|
'albumAPI': self.downloadObject.collection.get('albumAPI'),
|
|
|
|
'playlistAPI': self.downloadObject.collection.get('playlistAPI')
|
|
|
|
})
|
|
|
|
self.afterDownloadCollection(tracks)
|
|
|
|
|
|
|
|
if self.listener:
|
2021-07-25 11:56:34 +00:00
|
|
|
if self.downloadObject.isCanceled:
|
2021-06-27 20:29:41 +00:00
|
|
|
self.listener.send('currentItemCancelled', self.downloadObject.uuid)
|
|
|
|
self.listener.send("removedFromQueue", self.downloadObject.uuid)
|
|
|
|
else:
|
|
|
|
self.listener.send("finishDownload", self.downloadObject.uuid)
|
|
|
|
|
2021-06-28 23:37:34 +00:00
|
|
|
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})
|
|
|
|
|
2021-06-27 20:29:41 +00:00
|
|
|
def download(self, extraData, track=None):
|
|
|
|
returnData = {}
|
|
|
|
trackAPI_gw = extraData['trackAPI_gw']
|
|
|
|
trackAPI = extraData.get('trackAPI')
|
|
|
|
albumAPI = extraData.get('albumAPI')
|
|
|
|
playlistAPI = extraData.get('playlistAPI')
|
|
|
|
if self.downloadObject.isCanceled: raise DownloadCanceled
|
|
|
|
if trackAPI_gw['SNG_ID'] == "0": raise DownloadFailed("notOnDeezer")
|
|
|
|
|
2021-06-28 23:37:34 +00:00
|
|
|
itemData = {
|
|
|
|
'id': trackAPI_gw['SNG_ID'],
|
|
|
|
'title': trackAPI_gw['SNG_TITLE'].strip(),
|
|
|
|
'artist': trackAPI_gw['ART_NAME']
|
|
|
|
}
|
2021-06-27 20:29:41 +00:00
|
|
|
|
|
|
|
# Create Track object
|
|
|
|
if not track:
|
2021-06-28 23:37:34 +00:00
|
|
|
self.log(itemData, "getTags")
|
2021-06-27 20:29:41 +00:00
|
|
|
try:
|
|
|
|
track = Track().parseData(
|
|
|
|
dz=self.dz,
|
|
|
|
trackAPI_gw=trackAPI_gw,
|
|
|
|
trackAPI=trackAPI,
|
|
|
|
albumAPI=albumAPI,
|
|
|
|
playlistAPI=playlistAPI
|
|
|
|
)
|
|
|
|
except AlbumDoesntExists as e:
|
|
|
|
raise DownloadError('albumDoesntExists') from e
|
|
|
|
except MD5NotFound as e:
|
|
|
|
raise DownloadError('notLoggedIn') from e
|
2021-06-28 23:37:34 +00:00
|
|
|
self.log(itemData, "gotTags")
|
2021-06-27 20:29:41 +00:00
|
|
|
|
2021-06-28 23:37:34 +00:00
|
|
|
itemData = {
|
|
|
|
'id': track.id,
|
|
|
|
'title': track.title,
|
|
|
|
'artist': track.mainArtist.name
|
|
|
|
}
|
2021-06-27 20:29:41 +00:00
|
|
|
|
|
|
|
# Check if track not yet encoded
|
|
|
|
if track.MD5 == '': raise DownloadFailed("notEncoded", track)
|
|
|
|
|
|
|
|
# Choose the target bitrate
|
2021-06-28 23:37:34 +00:00
|
|
|
self.log(itemData, "getBitrate")
|
2021-06-27 20:29:41 +00:00
|
|
|
try:
|
|
|
|
selectedFormat = getPreferredBitrate(
|
2021-07-25 11:09:12 +00:00
|
|
|
self.dz,
|
2021-06-27 20:29:41 +00:00
|
|
|
track,
|
|
|
|
self.bitrate,
|
|
|
|
self.settings['fallbackBitrate'],
|
2021-08-02 20:42:03 +00:00
|
|
|
self.downloadObject.uuid, self.listener
|
2021-06-27 20:29:41 +00:00
|
|
|
)
|
2021-07-25 09:34:20 +00:00
|
|
|
except WrongLicense as e:
|
|
|
|
raise DownloadFailed("wrongLicense") from e
|
2021-08-02 20:42:03 +00:00
|
|
|
except WrongGeolocation as e:
|
|
|
|
raise DownloadFailed("wrongGeolocation") from e
|
2021-06-27 20:29:41 +00:00
|
|
|
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
|
2021-06-28 23:37:34 +00:00
|
|
|
self.log(itemData, "gotBitrate")
|
2021-06-27 20:29:41 +00:00
|
|
|
|
|
|
|
# Apply settings
|
|
|
|
track.applySettings(self.settings)
|
|
|
|
|
|
|
|
# Generate filename and filepath from metadata
|
|
|
|
(filename, filepath, artistPath, coverPath, extrasPath) = generatePath(track, self.downloadObject, self.settings)
|
|
|
|
|
|
|
|
# Make sure the filepath exists
|
|
|
|
makedirs(filepath, exist_ok=True)
|
|
|
|
extension = extensions[track.bitrate]
|
|
|
|
writepath = filepath / f"{filename}{extension}"
|
|
|
|
|
|
|
|
# Save extrasPath
|
2021-08-02 20:09:28 +00:00
|
|
|
if extrasPath and not self.downloadObject.extrasPath: self.downloadObject.extrasPath = extrasPath
|
2021-06-27 20:29:41 +00:00
|
|
|
|
|
|
|
# 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}")
|
|
|
|
|
|
|
|
# Download and cache coverart
|
2021-06-28 23:37:34 +00:00
|
|
|
self.log(itemData, "getAlbumArt")
|
2021-06-27 20:29:41 +00:00
|
|
|
track.album.embeddedCoverPath = downloadImage(track.album.embeddedCoverURL, track.album.embeddedCoverPath)
|
2021-06-28 23:37:34 +00:00
|
|
|
self.log(itemData, "gotAlbumArt")
|
2021-06-27 20:29:41 +00:00
|
|
|
|
|
|
|
# Save local album art
|
|
|
|
if coverPath:
|
|
|
|
returnData['albumURLs'] = []
|
|
|
|
for pic_format in self.settings['localArtworkFormat'].split(","):
|
|
|
|
if pic_format in ["png","jpg"]:
|
|
|
|
extendedFormat = pic_format
|
|
|
|
if extendedFormat == "jpg": extendedFormat += f"-{self.settings['jpegImageQuality']}"
|
|
|
|
url = track.album.pic.getURL(self.settings['localArtworkSize'], extendedFormat)
|
|
|
|
# Skip non deezer pictures at the wrong format
|
|
|
|
if isinstance(track.album.pic, StaticPicture) and pic_format != "jpg":
|
|
|
|
continue
|
|
|
|
returnData['albumURLs'].append({'url': url, 'ext': pic_format})
|
|
|
|
returnData['albumPath'] = coverPath
|
|
|
|
returnData['albumFilename'] = generateAlbumName(self.settings['coverImageTemplate'], track.album, self.settings, track.playlist)
|
|
|
|
|
|
|
|
# Save artist art
|
|
|
|
if artistPath:
|
|
|
|
returnData['artistURLs'] = []
|
|
|
|
for pic_format in self.settings['localArtworkFormat'].split(","):
|
|
|
|
# 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)
|
|
|
|
|
|
|
|
# Save playlist art
|
|
|
|
if track.playlist:
|
|
|
|
if len(self.playlistURLs) == 0:
|
|
|
|
for pic_format in self.settings['localArtworkFormat'].split(","):
|
|
|
|
if pic_format in ["png","jpg"]:
|
|
|
|
extendedFormat = pic_format
|
|
|
|
if extendedFormat == "jpg": extendedFormat += f"-{self.settings['jpegImageQuality']}"
|
|
|
|
url = track.playlist.pic.getURL(self.settings['localArtworkSize'], extendedFormat)
|
|
|
|
if isinstance(track.playlist.pic, StaticPicture) and pic_format != "jpg": continue
|
|
|
|
self.playlistURLs.append({'url': url, 'ext': pic_format})
|
|
|
|
if not self.playlistCoverName:
|
|
|
|
track.playlist.bitrate = selectedFormat
|
|
|
|
track.playlist.dateString = track.playlist.date.format(self.settings['dateFormat'])
|
|
|
|
self.playlistCoverName = generateAlbumName(self.settings['coverImageTemplate'], track.playlist, self.settings, track.playlist)
|
|
|
|
|
|
|
|
# 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'))
|
|
|
|
|
|
|
|
# Check for overwrite settings
|
|
|
|
trackAlreadyDownloaded = writepath.is_file()
|
|
|
|
|
|
|
|
# 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()
|
|
|
|
if trackAlreadyDownloaded: break
|
|
|
|
# Don't overwrite and keep both files
|
|
|
|
if trackAlreadyDownloaded and self.settings['overwriteFile'] == OverwriteOption.KEEP_BOTH:
|
|
|
|
baseFilename = str(filepath / filename)
|
|
|
|
c = 1
|
|
|
|
currentFilename = baseFilename+' ('+str(c)+')'+ extension
|
|
|
|
while Path(currentFilename).is_file():
|
|
|
|
c += 1
|
|
|
|
currentFilename = baseFilename+' ('+str(c)+')'+ extension
|
|
|
|
trackAlreadyDownloaded = False
|
|
|
|
writepath = Path(currentFilename)
|
|
|
|
|
|
|
|
if not trackAlreadyDownloaded or self.settings['overwriteFile'] == OverwriteOption.OVERWRITE:
|
2021-07-25 10:51:46 +00:00
|
|
|
track.downloadURL = track.urls[formatsName[track.bitrate]]
|
2021-09-21 12:33:54 +00:00
|
|
|
if not track.downloadURL: raise DownloadFailed('notAvailable', track)
|
2021-06-27 20:29:41 +00:00
|
|
|
try:
|
|
|
|
with open(writepath, 'wb') as stream:
|
|
|
|
streamTrack(stream, track, downloadObject=self.downloadObject, listener=self.listener)
|
|
|
|
except requests.exceptions.HTTPError as e:
|
2021-06-28 23:11:13 +00:00
|
|
|
if writepath.is_file(): writepath.unlink()
|
2021-06-27 20:29:41 +00:00
|
|
|
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
|
2021-06-28 23:37:34 +00:00
|
|
|
self.log(itemData, "downloaded")
|
2021-06-27 20:29:41 +00:00
|
|
|
else:
|
2021-06-28 23:37:34 +00:00
|
|
|
self.log(itemData, "alreadyDownloaded")
|
2021-06-27 20:29:41 +00:00
|
|
|
self.downloadObject.completeTrackProgress(self.listener)
|
|
|
|
|
|
|
|
# Adding tags
|
|
|
|
if (not trackAlreadyDownloaded or self.settings['overwriteFile'] in [OverwriteOption.ONLY_TAGS, OverwriteOption.OVERWRITE]) and not track.local:
|
2021-06-28 23:37:34 +00:00
|
|
|
self.log(itemData, "tagging")
|
2021-06-27 20:29:41 +00:00
|
|
|
if extension == '.mp3':
|
|
|
|
tagID3(writepath, track, self.settings['tags'])
|
|
|
|
elif extension == '.flac':
|
|
|
|
try:
|
|
|
|
tagFLAC(writepath, track, self.settings['tags'])
|
|
|
|
except (FLACNoHeaderError, FLACError):
|
|
|
|
writepath.unlink()
|
2021-06-28 23:37:34 +00:00
|
|
|
logger.warning("%s Track not available in FLAC, falling back if necessary", f"{itemData['artist']} - {itemData['title']}")
|
2021-06-27 20:29:41 +00:00
|
|
|
self.downloadObject.removeTrackProgress(self.listener)
|
|
|
|
track.filesizes['FILESIZE_FLAC'] = "0"
|
|
|
|
track.filesizes['FILESIZE_FLAC_TESTED'] = True
|
|
|
|
return self.download(trackAPI_gw, track=track)
|
2021-06-28 23:37:34 +00:00
|
|
|
self.log(itemData, "tagged")
|
2021-06-27 20:29:41 +00:00
|
|
|
|
|
|
|
if track.searched: returnData['searched'] = True
|
|
|
|
self.downloadObject.downloaded += 1
|
|
|
|
if self.listener: self.listener.send("updateQueue", {
|
|
|
|
'uuid': self.downloadObject.uuid,
|
|
|
|
'downloaded': True,
|
|
|
|
'downloadPath': str(writepath),
|
2021-08-02 20:09:28 +00:00
|
|
|
'extrasPath': str(self.downloadObject.extrasPath)
|
2021-06-27 20:29:41 +00:00
|
|
|
})
|
|
|
|
returnData['filename'] = str(writepath)[len(str(extrasPath))+ len(pathSep):]
|
2021-06-28 23:37:34 +00:00
|
|
|
returnData['data'] = itemData
|
2021-09-02 22:04:17 +00:00
|
|
|
returnData['path'] = str(writepath)
|
|
|
|
self.downloadObject.files.append(returnData)
|
2021-06-27 20:29:41 +00:00
|
|
|
return returnData
|
|
|
|
|
|
|
|
def downloadWrapper(self, extraData, track=None):
|
|
|
|
trackAPI_gw = extraData['trackAPI_gw']
|
2021-08-04 19:36:34 +00:00
|
|
|
if trackAPI_gw.get('_EXTRA_TRACK'):
|
2021-06-27 20:29:41 +00:00
|
|
|
extraData['trackAPI'] = trackAPI_gw['_EXTRA_TRACK'].copy()
|
|
|
|
del extraData['trackAPI_gw']['_EXTRA_TRACK']
|
|
|
|
# Temp metadata to generate logs
|
2021-06-28 23:37:34 +00:00
|
|
|
itemData = {
|
2021-06-27 20:29:41 +00:00
|
|
|
'id': trackAPI_gw['SNG_ID'],
|
|
|
|
'title': trackAPI_gw['SNG_TITLE'].strip(),
|
|
|
|
'artist': trackAPI_gw['ART_NAME']
|
|
|
|
}
|
|
|
|
if trackAPI_gw.get('VERSION') and trackAPI_gw['VERSION'] not in trackAPI_gw['SNG_TITLE']:
|
2021-06-28 23:37:34 +00:00
|
|
|
itemData['title'] += f" {trackAPI_gw['VERSION']}".strip()
|
2021-06-27 20:29:41 +00:00
|
|
|
|
|
|
|
try:
|
|
|
|
result = self.download(extraData, track)
|
|
|
|
except DownloadFailed as error:
|
|
|
|
if error.track:
|
|
|
|
track = error.track
|
|
|
|
if track.fallbackID != "0":
|
2021-06-28 23:37:34 +00:00
|
|
|
self.warn(itemData, error.errid, 'fallback')
|
2021-06-27 20:29:41 +00:00
|
|
|
newTrack = self.dz.gw.get_track_with_fallback(track.fallbackID)
|
|
|
|
track.parseEssentialData(newTrack)
|
|
|
|
track.retriveFilesizes(self.dz)
|
|
|
|
return self.downloadWrapper(extraData, track)
|
|
|
|
if not track.searched and self.settings['fallbackSearch']:
|
2021-06-28 23:37:34 +00:00
|
|
|
self.warn(itemData, error.errid, 'search')
|
2021-06-27 20:29:41 +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-08-02 21:45:08 +00:00
|
|
|
self.log(itemData, "searchFallback")
|
2021-06-27 20:29:41 +00:00
|
|
|
return self.downloadWrapper(extraData, track)
|
|
|
|
error.errid += "NoAlternative"
|
2021-08-02 19:55:22 +00:00
|
|
|
error.message = ErrorMessages[error.errid]
|
2021-06-27 20:29:41 +00:00
|
|
|
result = {'error': {
|
|
|
|
'message': error.message,
|
|
|
|
'errid': error.errid,
|
2021-06-28 23:37:34 +00:00
|
|
|
'data': itemData
|
2021-06-27 20:29:41 +00:00
|
|
|
}}
|
|
|
|
except Exception as e:
|
2021-06-28 23:37:34 +00:00
|
|
|
logger.exception("%s %s", f"{itemData['artist']} - {itemData['title']}", e)
|
2021-06-27 20:29:41 +00:00
|
|
|
result = {'error': {
|
|
|
|
'message': str(e),
|
2021-09-21 06:57:16 +00:00
|
|
|
'data': itemData,
|
|
|
|
'stack': traceback.format_exc()
|
2021-06-27 20:29:41 +00:00
|
|
|
}}
|
|
|
|
|
|
|
|
if 'error' in result:
|
|
|
|
self.downloadObject.completeTrackProgress(self.listener)
|
|
|
|
self.downloadObject.failed += 1
|
|
|
|
self.downloadObject.errors.append(result['error'])
|
|
|
|
if self.listener:
|
|
|
|
error = result['error']
|
|
|
|
self.listener.send("updateQueue", {
|
|
|
|
'uuid': self.downloadObject.uuid,
|
|
|
|
'failed': True,
|
|
|
|
'data': error['data'],
|
|
|
|
'error': error['message'],
|
2021-09-21 06:57:16 +00:00
|
|
|
'errid': error['errid'] if 'errid' in error else None,
|
|
|
|
'stack': error['stack'] if 'stack' in error else None
|
2021-06-27 20:29:41 +00:00
|
|
|
})
|
|
|
|
return result
|
|
|
|
|
|
|
|
def afterDownloadSingle(self, track):
|
2021-08-02 20:09:28 +00:00
|
|
|
if not self.downloadObject.extrasPath: self.downloadObject.extrasPath = Path(self.settings['downloadLocation'])
|
2021-06-27 20:29:41 +00:00
|
|
|
|
|
|
|
# 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'])
|
|
|
|
|
|
|
|
# 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'])
|
|
|
|
|
|
|
|
# Create searched logfile
|
|
|
|
if self.settings['logSearched'] and 'searched' in track:
|
|
|
|
filename = f"{track.data.artist} - {track.data.title}"
|
2021-08-02 20:09:28 +00:00
|
|
|
with open(self.downloadObject.extrasPath / 'searched.txt', 'wb+') as f:
|
2021-06-27 20:29:41 +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'))
|
|
|
|
|
|
|
|
# Execute command after download
|
|
|
|
if self.settings['executeCommand'] != "":
|
2021-08-02 20:09:28 +00:00
|
|
|
execute(self.settings['executeCommand'].replace("%folder%", quote(str(self.downloadObject.extrasPath))).replace("%filename%", quote(track['filename'])))
|
2021-06-27 20:29:41 +00:00
|
|
|
|
|
|
|
def afterDownloadCollection(self, tracks):
|
2021-08-02 20:09:28 +00:00
|
|
|
if not self.downloadObject.extrasPath: self.downloadObject.extrasPath = Path(self.settings['downloadLocation'])
|
2021-06-27 20:29:41 +00:00
|
|
|
playlist = [None] * len(tracks)
|
|
|
|
errors = ""
|
|
|
|
searched = ""
|
|
|
|
|
|
|
|
for i, track in enumerate(tracks):
|
|
|
|
track = track.result()
|
|
|
|
if not track: return # Check if item is cancelled
|
|
|
|
|
|
|
|
# Log errors to file
|
|
|
|
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"
|
|
|
|
|
|
|
|
# Log searched to file
|
|
|
|
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'])
|
|
|
|
|
|
|
|
# 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'])
|
|
|
|
|
|
|
|
# Save filename for playlist file
|
|
|
|
playlist[i] = track.get('filename', "")
|
|
|
|
|
|
|
|
# Create errors logfile
|
|
|
|
if self.settings['logErrors'] and errors != "":
|
2021-08-02 20:09:28 +00:00
|
|
|
with open(self.downloadObject.extrasPath / 'errors.txt', 'wb') as f:
|
2021-06-27 20:29:41 +00:00
|
|
|
f.write(errors.encode('utf-8'))
|
|
|
|
|
|
|
|
# Create searched logfile
|
|
|
|
if self.settings['logSearched'] and searched != "":
|
2021-08-02 20:09:28 +00:00
|
|
|
with open(self.downloadObject.extrasPath / 'searched.txt', 'wb') as f:
|
2021-06-27 20:29:41 +00:00
|
|
|
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:
|
2021-08-02 20:09:28 +00:00
|
|
|
downloadImage(image['url'], self.downloadObject.extrasPath / f"{self.playlistCoverName}.{image['ext']}", self.settings['overwriteFile'])
|
2021-06-27 20:29:41 +00:00
|
|
|
|
|
|
|
# Create M3U8 File
|
|
|
|
if self.settings['createM3U8File']:
|
|
|
|
filename = generateDownloadObjectName(self.settings['playlistFilenameTemplate'], self.downloadObject, self.settings) or "playlist"
|
2021-08-02 20:09:28 +00:00
|
|
|
with open(self.downloadObject.extrasPath / f'{filename}.m3u8', 'wb') as f:
|
2021-06-27 20:29:41 +00:00
|
|
|
for line in playlist:
|
|
|
|
f.write((line + "\n").encode('utf-8'))
|
|
|
|
|
|
|
|
# Execute command after download
|
|
|
|
if self.settings['executeCommand'] != "":
|
2021-08-02 20:09:28 +00:00
|
|
|
execute(self.settings['executeCommand'].replace("%folder%", quote(str(self.downloadObject.extrasPath))))
|