From 4cfdc4872dd1d308ec045f2557f5383052d8876a Mon Sep 17 00:00:00 2001 From: RemixDev Date: Sat, 15 Aug 2020 21:34:10 +0200 Subject: [PATCH] Done more code rework --- deemix/app/DownloadJob.py | 636 ++++++++++ .../app/{queuemanager.py => QueueManager.py} | 0 deemix/app/{spotify.py => SpotifyHelper.py} | 0 deemix/app/Track.py | 354 ++++++ deemix/app/downloader.py | 1075 ----------------- deemix/utils/misc.py | 25 + deemix/utils/pathtemplates.py | 64 +- deemix/utils/taggers.py | 132 +- 8 files changed, 1113 insertions(+), 1173 deletions(-) create mode 100644 deemix/app/DownloadJob.py rename deemix/app/{queuemanager.py => QueueManager.py} (100%) rename deemix/app/{spotify.py => SpotifyHelper.py} (100%) create mode 100644 deemix/app/Track.py delete mode 100644 deemix/app/downloader.py diff --git a/deemix/app/DownloadJob.py b/deemix/app/DownloadJob.py new file mode 100644 index 0000000..33d8f15 --- /dev/null +++ b/deemix/app/DownloadJob.py @@ -0,0 +1,636 @@ +#!/usr/bin/env python3 +import os.path +import re + +from requests import get +from requests.exceptions import HTTPError, ConnectionError + +from concurrent.futures import ThreadPoolExecutor +from os import makedirs, remove, system as execute +from tempfile import gettempdir +from time import sleep + +from deemix.app.queueitem import QIConvertable, QISingle, QICollection +from deemix.app.Track import Track +from deemix.utils.misc import changeCase +from deemix.utils.pathtemplates import generateFilename, generateFilepath, settingsRegexAlbum, settingsRegexArtist, settingsRegexPlaylistFile +from deemix.api.deezer import USER_AGENT_HEADER +from deemix.utils.taggers import tagID3, tagFLAC + +from Cryptodome.Cipher import Blowfish +from mutagen.flac import FLACNoHeaderError +import logging + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger('deemix') + +TEMPDIR = os.path.join(gettempdir(), 'deemix-imgs') +if not os.path.isdir(TEMPDIR): + makedirs(TEMPDIR) + +extensions = { + 9: '.flac', + 0: '.mp3', + 3: '.mp3', + 1: '.mp3', + 8: '.mp3', + 15: '.mp4', + 14: '.mp4', + 13: '.mp4' +} + +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!", +} + +def after_download(tracks, settings, queueItem): + extrasPath = None + playlist = [None] * len(tracks) + playlistCover = None + playlistURLs = [] + errors = "" + searched = "" + for index in range(len(tracks)): + result = tracks[index].result() + if 'cancel' in result: + return None + if 'error' in result: + if not 'data' in result['error']: + result['error']['data'] = {'id': 0, 'title': 'Unknown', 'artist': 'Unknown'} + errors += f"{result['error']['data']['id']} | {result['error']['data']['artist']} - {result['error']['data']['title']} | {result['error']['message']}\r\n" + if 'searched' in result: + searched += result['searched'] + "\r\n" + if not extrasPath and 'extrasPath' in result: + extrasPath = result['extrasPath'] + if not playlistCover and 'playlistCover' in result: + playlistCover = result['playlistCover'] + playlistURLs = result['playlistURLs'] + if settings['saveArtwork'] and 'albumPath' in result: + for image in result['albumURLs']: + downloadImage(image['url'], f"{result['albumPath']}.{image['ext']}", settings['overwriteFile']) + if settings['saveArtworkArtist'] and 'artistPath' in result: + for image in result['artistURLs']: + downloadImage(image['url'], f"{result['artistPath']}.{image['ext']}", settings['overwriteFile']) + if 'playlistPosition' in result: + playlist[index] = result['playlistPosition'] + else: + playlist[index] = "" + if not extrasPath: + extrasPath = settings['downloadLocation'] + if settings['logErrors'] and errors != "": + with open(os.path.join(extrasPath, 'errors.txt'), 'wb') as f: + f.write(errors.encode('utf-8')) + if settings['saveArtwork'] and playlistCover and not settings['tags']['savePlaylistAsCompilation']: + for image in playlistURLs: + downloadImage(image['url'], os.path.join(extrasPath, playlistCover)+f".{image['ext']}", settings['overwriteFile']) + if settings['logSearched'] and searched != "": + with open(os.path.join(extrasPath, 'searched.txt'), 'wb') as f: + f.write(searched.encode('utf-8')) + if settings['createM3U8File']: + filename = settingsRegexPlaylistFile(settings['playlistFilenameTemplate'], queueItem, settings) or "playlist" + with open(os.path.join(extrasPath, filename+'.m3u8'), 'wb') as f: + for line in playlist: + f.write((line + "\n").encode('utf-8')) + if settings['executeCommand'] != "": + execute(settings['executeCommand'].replace("%folder%", extrasPath)) + return extrasPath + + +def after_download_single(track, settings): + if 'cancel' in track: + return None + if 'extrasPath' not in track: + track['extrasPath'] = settings['downloadLocation'] + if settings['saveArtwork'] and 'albumPath' in track: + for image in track['albumURLs']: + downloadImage(image['url'], f"{track['albumPath']}.{image['ext']}", settings['overwriteFile']) + if settings['saveArtworkArtist'] and 'artistPath' in track: + for image in track['artistURLs']: + downloadImage(image['url'], f"{track['artistPath']}.{image['ext']}", settings['overwriteFile']) + if settings['logSearched'] and 'searched' in track: + with open(os.path.join(track['extrasPath'], 'searched.txt'), 'wb+') as f: + orig = f.read().decode('utf-8') + if not track['searched'] in orig: + if orig != "": + orig += "\r\n" + orig += track['searched'] + "\r\n" + f.write(orig.encode('utf-8')) + if settings['executeCommand'] != "": + execute(settings['executeCommand'].replace("%folder%", track['extrasPath']).replace("%filename%", track['playlistPosition'])) + return track['extrasPath'] + +def downloadImage(url, path, overwrite="n"): + if not os.path.isfile(path) or overwrite in ['y', 't', 'b']: + 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 HTTPError: + 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: + logger.warn("Couldn't download "+str(pictureSize)+"x"+str(pictureSize)+" image, falling back to 1200x1200") + sleep(1) + return downloadImage(urlBase+pictureUrl.replace(str(pictureSize)+"x"+str(pictureSize), '1200x1200'), path, overwrite) + logger.error("Couldn't download Image: "+url) + except: + sleep(1) + return downloadImage(url, path, overwrite) + remove(path) + return None + else: + return path + +def formatDate(date, template): + elements = { + 'year': ['YYYY', 'YY', 'Y'], + 'month': ['MM', 'M'], + 'day': ['DD', 'D'] + } + for element, placeholders in elements.items(): + for placeholder in placeholders: + if placeholder in template: + template = template.replace(placeholder, str(date[element])) + return template + +class DownloadJob: + def __init__(self, dz, sp, queueItem, interface=None): + self.dz = dz + self.sp = sp + self.queueItem = queueItem + self.interface = interface + self.settings = queueItem.settings + self.bitrate = queueItem.bitrate + self.downloadPercentage = 0 + self.lastPercentage = 0 + self.extrasPath = self.settings['downloadLocation'] + + def start(self): + if isinstance(self.queueItem, QIConvertable): + self.sp.convert_spotify_playlist(self.dz, self.queueItem, self.settings, interface=self.interface) + if isinstance(self.queueItem, QISingle): + result = self.downloadWrapper(self.queueItem.single) + if result: + download_path = after_download_single(result, self.settings) + elif isinstance(self.queueItem, QICollection): + playlist = [None] * len(self.queueItem.collection) + with ThreadPoolExecutor(self.settings['queueConcurrency']) as executor: + for pos, track in enumerate(self.queueItem.collection, start=0): + playlist[pos] = executor.submit(self.downloadWrapper, track) + download_path = after_download(playlist, self.settings, self.queueItem) + if self.interface: + if self.queueItem.cancel: + self.interface.send('currentItemCancelled', self.queueItem.uuid) + self.interface.send("removedFromQueue", self.queueItem.uuid) + else: + self.interface.send("finishDownload", self.queueItem.uuid) + return download_path + + def download(self, trackAPI_gw, track=None): + result = {} + if self.queueItem.cancel: raise DownloadCancelled + + if trackAPI_gw['SNG_ID'] == "0": + raise DownloadFailed("notOnDeezer") + + # Create Track object + if not track: + logger.info(f"[{trackAPI_gw['ART_NAME']} - {trackAPI_gw['SNG_TITLE']}] Getting the tags") + track = Track(self.dz, + settings=self.settings, + trackAPI_gw=trackAPI_gw, + trackAPI=trackAPI_gw['_EXTRA_TRACK'] if '_EXTRA_TRACK' in trackAPI_gw else None, + albumAPI=trackAPI_gw['_EXTRA_ALBUM'] if '_EXTRA_ALBUM' in trackAPI_gw else None + ) + if self.queueItem.cancel: raise DownloadCancelled + + if self.MD5 == '': + if track.fallbackId != "0": + logger.warn(f"[{track.mainArtist['name']} - {track.title}] Track not yet encoded, using fallback id") + newTrack = self.dz.get_track_gw(track.fallbackId) + track.parseEssentialData(self.dz, newTrack) + return self.download(trackAPI_gw, track) + elif not track.searched and self.settings['fallbackSearch']: + logger.warn(f"[{self.mainArtist['name']} - {self.title}] Track not yet encoded, searching for alternative") + searchedId = self.dz.get_track_from_metadata(self.mainArtist['name'], self.title, self.album['title']) + if searchedId != 0: + newTrack = self.dz.get_track_gw(searchedId) + track.parseEssentialData(self.dz, newTrack) + track.searched = True + return self.download(trackAPI_gw, track) + else: + raise DownloadFailed("notEncodedNoAlternative") + else: + raise DownloadFailed("notEncoded") + + selectedFormat = self.getPreferredBitrate(track) + if selectedFormat == -100: + if track.fallbackId != "0": + logger.warn(f"[{track.mainArtist['name']} - {track.title}] Track not found at desired bitrate, using fallback id") + newTrack = self.dz.get_track_gw(track.fallbackId) + track.parseEssentialData(self.dz, newTrack) + return self.download(trackAPI_gw, track) + elif not track.searched and self.settings['fallbackSearch']: + logger.warn(f"[{self.mainArtist['name']} - {self.title}] Track not found at desired bitrate, searching for alternative") + searchedId = self.dz.get_track_from_metadata(self.mainArtist['name'], self.title, self.album['title']) + if searchedId != 0: + newTrack = self.dz.get_track_gw(searchedId) + track.parseEssentialData(self.dz, newTrack) + track.searched = True + return self.download(trackAPI_gw, track) + else: + raise DownloadFailed("wrongBitrateNoAlternative") + else: + raise DownloadFailed("wrongBitrate") + elif selectedFormat == -200: + raise DownloadFailed("no360RA") + track.selectedFormat = selectedFormat + + if self.settings['tags']['savePlaylistAsCompilation'] and track.playlist: + track.trackNumber = track.position + track.discNumber = "1" + track.album = {**track.album, **track.playlist} + track.album['picPath'] = os.path.join(TEMPDIR, + f"pl{trackAPI_gw['_EXTRA_PLAYLIST']['id']}_{settings['embeddedArtworkSize']}.jpg") + else: + if track.album['date']: + track.date = track.album['date'] + track.album['picUrl'] = "https://e-cdns-images.dzcdn.net/images/cover/{}/{}x{}-{}".format( + track.album['pic'], + self.settings['embeddedArtworkSize'], self.settings['embeddedArtworkSize'], + f'000000-{self.settings["jpegImageQuality"]}-0-0.jpg' + ) + track.album['bitrate'] = selectedFormat + + track.dateString = formatDate(track.date, settings['dateFormat']) + track.album['dateString'] = formatDate(track.album['date'], settings['dateFormat']) + + # Check if user wants the feat in the title + # 0 => do not change + # 1 => remove from title + # 2 => add to title + # 3 => remove from title and album title + if self.settings['featuredToTitle'] == "1": + track.title = track.getCleanTitle() + elif self.settings['featuredToTitle'] == "2": + track.title = track.getFeatTitle() + elif self.settings['featuredToTitle'] == "3": + track.title = track.getCleanTitle() + track.album['title'] = track.getCleanAlbumTitle() + + # Remove (Album Version) from tracks that have that + if self.settings['removeAlbumVersion']: + if "Album Version" in track.title: + track.title = re.sub(r' ?\(Album Version\)', "", track.title).strip() + + # Generate artist tag if needed + if self.settings['tags']['multiArtistSeparator'] != "default": + if self.settings['tags']['multiArtistSeparator'] == "andFeat": + track.artistsString = track.mainArtistsString + if track.featArtistsString and self.settings['featuredToTitle'] != "2": + track.artistsString += " " + track.featArtistsString + else: + track.artistsString = self.settings['tags']['multiArtistSeparator'].join(track.artists) + else: + track.artistsString = ", ".join(track.artists) + + # Change Title and Artists casing if needed + if self.settings['titleCasing'] != "nothing": + track.title = changeCase(track.title, self.settings['titleCasing']) + if self.settings['artistCasing'] != "nothing": + track.artistsString = changeCase(track.artistsString, self.settings['artistCasing']) + for i, artist in enumerate(track.artists): + track.artists[i] = changeCase(artist, self.settings['artistCasing']) + + # Generate filename and filepath from metadata + filename = generateFilename(track, trackAPI_gw, self.settings) + (filepath, artistPath, coverPath, extrasPath) = generateFilepath(track, trackAPI_gw, self.settings) + + if self.queueItem.cancel: raise DownloadCancelled + + # Download and cache coverart + if self.settings['tags']['savePlaylistAsCompilation'] and track.playlist: + + else: + track.album['picPath'] = os.path.join(TEMPDIR, + f"alb{track.album['id']}_{settings['embeddedArtworkSize']}.jpg") + logger.info(f"[{track.mainArtist['name']} - {track.title}] Getting the album cover") + track.album['picPath'] = downloadImage(track.album['picUrl'], track.album['picPath']) + + # Save local album art + if coverPath: + result['albumURLs'] = [] + for format in self.settings['localArtworkFormat'].split(","): + if format in ["png","jpg"]: + url = track.album['picUrl'].replace( + f"{self.settings['embeddedArtworkSize']}x{self.settings['embeddedArtworkSize']}", + f"{self.settings['localArtworkSize']}x{self.settings['localArtworkSize']}") + if format == "png": + url = url[:url.find("000000-")]+"none-100-0-0.png" + result['albumURLs'].append({'url': url, 'ext': format}) + result['albumPath'] = os.path.join(coverPath, + f"{settingsRegexAlbum(self.settings['coverImageTemplate'], track.album, self.settings, trackAPI_gw['_EXTRA_PLAYLIST'] if'_EXTRA_PLAYLIST' in trackAPI_gw else None)}") + + # Save artist art + if artistPath: + result['artistURLs'] = [] + for format in self.settings['localArtworkFormat'].split(","): + if format in ["png","jpg"]: + url = "" + if track.album['mainArtist']['pic'] != "": + url = "https://e-cdns-images.dzcdn.net/images/artist/{}/{}x{}-{}".format( + track.album['mainArtist']['pic'], self.settings['localArtworkSize'], self.settings['localArtworkSize'], + 'none-100-0-0.png' if format == "png" else f'000000-{self.settings["jpegImageQuality"]}-0-0.jpg') + elif format == "jpg": + url = "https://e-cdns-images.dzcdn.net/images/artist//{}x{}-{}".format( + self.settings['localArtworkSize'], self.settings['localArtworkSize'], f'000000-{self.settings["jpegImageQuality"]}-0-0.jpg') + if url: + result['artistURLs'].append({'url': url, 'ext': format}) + result['artistPath'] = os.path.join(artistPath, + f"{settingsRegexArtist(self.settings['artistImageTemplate'], track.album['mainArtist'], self.settings)}") + + # Remove subfolders from filename and add it to filepath + if os.path.sep in filename: + tempPath = filename[:filename.rfind(os.path.sep)] + filepath = os.path.join(filepath, tempPath) + filename = filename[filename.rfind(os.path.sep) + len(os.path.sep):] + + # Make sure the filepath exsists + makedirs(filepath, exist_ok=True) + writepath = os.path.join(filepath, filename + extensions[track.selectedFormat]) + + # Save lyrics in lrc file + if self.settings['syncedLyrics'] and 'sync' in track.lyrics: + if not os.path.isfile(os.path.join(filepath, filename + '.lrc')) or settings['overwriteFile'] in ['y', 't']: + with open(os.path.join(filepath, filename + '.lrc'), 'wb') as f: + f.write(track.lyrics['sync'].encode('utf-8')) + + trackAlreadyDownloaded = os.path.isfile(writepath) + if trackAlreadyDownloaded and self.settings['overwriteFile'] == 'b': + baseFilename = os.path.join(filepath, filename) + i = 1 + currentFilename = baseFilename+' ('+str(i)+')'+ extensions[track.selectedFormat] + while os.path.isfile(currentFilename): + i += 1 + currentFilename = baseFilename+' ('+str(i)+')'+ extensions[track.selectedFormat] + trackAlreadyDownloaded = False + writepath = currentFilename + + + if extrasPath: + if not self.extrasPath: + self.extrasPath = extrasPath + result['extrasPath'] = extrasPath + + # Data for m3u file + result['playlistPosition'] = writepath[len(extrasPath):] + + # Save playlist cover + if track.playlist: + result['playlistURLs'] = [] + if 'dzcdn.net' in track.playlist['picUrl']: + for format in self.settings['localArtworkFormat'].split(","): + if format in ["png","jpg"]: + url = track.playlist['picUrl'].replace( + f"{self.settings['embeddedArtworkSize']}x{self.settings['embeddedArtworkSize']}", + f"{self.settings['localArtworkSize']}x{self.settings['localArtworkSize']}") + if format == "png": + url = url[:url.find("000000-")]+"none-100-0-0.png" + result['playlistURLs'].append({'url': url, 'ext': format}) + else: + result['playlistURLs'].append({'url': track.playlist['picUrl'], 'ext': 'jpg'}) + track.playlist['id'] = "pl_" + str(trackAPI_gw['_EXTRA_PLAYLIST']['id']) + track.playlist['genre'] = ["Compilation", ] + track.playlist['bitrate'] = selectedFormat + track.playlist['dateString'] = formatDate(track.playlist['date'], self.settings['dateFormat']) + result['playlistCover'] = f"{settingsRegexAlbum(self.settings['coverImageTemplate'], track.playlist, self.settings, trackAPI_gw['_EXTRA_PLAYLIST'])}" + + if not trackAlreadyDownloaded or self.settings['overwriteFile'] == 'y': + logger.info(f"[{track.mainArtist['name']} - {track.title}] Downloading the track") + track.downloadUrl = dz.get_track_stream_url(track.id, track.MD5, track.mediaVersion, track.selectedFormat) + + def downloadMusic(track, trackAPI_gw): + try: + with open(writepath, 'wb') as stream: + self.streamTrack(stream, track, trackAPI_gw) + except DownloadCancelled: + remove(writepath) + raise DownloadCancelled + except HTTPError: + remove(writepath) + if track.fallbackId != "0": + logger.warn(f"[{track.mainArtist['name']} - {track.title}] Track not available, using fallback id") + newTrack = self.dz.get_track_gw(track.fallbackId) + track.parseEssentialData(self.dz, newTrack) + return False + elif not track.searched and self.settings['fallbackSearch']: + logger.warn(f"[{self.mainArtist['name']} - {self.title}] Track not available, searching for alternative") + searchedId = self.dz.get_track_from_metadata(self.mainArtist['name'], self.title, self.album['title']) + if searchedId != 0: + newTrack = self.dz.get_track_gw(searchedId) + track.parseEssentialData(self.dz, newTrack) + track.searched = True + return False + else: + raise DownloadFailed("notAvailableNoAlternative") + else: + raise DownloadFailed("notAvailable") + except ConnectionError as e: + logger.exception(str(e)) + logger.warn(f"[{track.mainArtist['name']} - {track.title}] Error while downloading the track, trying again in 5s...") + sleep(5) + return downloadMusic(track, trackAPI_gw) + except Exception as e: + logger.exception(str(e)) + logger.warn(f"[{track.mainArtist['name']} - {track.title}] Error while downloading the track, you should report this to the developers") + raise e + return True + + try: + trackDownloaded = downloadMusic(track, trackAPI_gw) + except DownloadFailed as e: + raise DownloadFailed + except Exception as e: + raise e + + if not trackDownloaded: + return self.download(trackAPI_gw, track) + else: + logger.info(f"[{track.mainArtist['name']} - {track.title}] Skipping track as it's already downloaded") + trackCompletePercentage(trackAPI, queueItem, interface) + + # Adding tags + if (not trackAlreadyDownloaded or self.settings['overwriteFile'] in ['t', 'y']) and not track.localTrack: + logger.info(f"[{track.mainArtist['name']} - {track.title}] Applying tags to the track") + if track.selectedFormat in [3, 1, 8]: + tagID3(writepath, track, self.settings['tags']) + elif track.selectedFormat == 9: + try: + tagFLAC(writepath, track, self.settings['tags']) + except FLACNoHeaderError: + remove(writepath) + logger.warn(f"[{track.mainArtist['name']} - {track.title}] Track not available in FLAC, falling back if necessary") + self.removeTrackPercentage(trackAPI, queueItem, interface) + track.formats['FILESIZE_FLAC'] = "0" + return self.download(trackAPI_gw, track) + if track.searched: + result['searched'] = f'{track.mainArtist['name']} - {track.title}' + + logger.info(f"[{track.mainArtist['name']} - {track.title}] Track download completed") + self.queueItem.downloaded += 1 + if self.interface: + self.interface.send("updateQueue", {'uuid': queueItem.uuid, 'downloaded': True, 'downloadPath': writepath}) + return result + + def getPreferredBitrate(self, track): + if track.localTrack: + return 0 + + fallback = self.settings['fallbackBitrate'] + + formats_non_360 = { + 9: "FLAC", + 3: "MP3_320", + 1: "MP3_128", + } + formats_360 = { + 15: "MP4_RA3", + 14: "MP4_RA2", + 13: "MP4_RA1", + } + + if not fallback: + error_num = -100 + formats = formats_360 + formats.update(formats_non_360) + elif int(self.bitrate) in formats_360: + error_num = -200 + formats = formats_360 + else: + error_num = 8 + formats = formats_non_360 + + for format_num, format in formats.items(): + if format_num <= int(self.bitrate): + if f"FILESIZE_{format}" in track.filesizes and int(track.filesizes[f"FILESIZE_{format}"]) != 0: + return format_num + else: + if fallback: + continue + else: + return error_num + + return error_num # fallback is enabled and loop went through all formats + + def stream_track(self, stream, track, trackAPI): + if self.queueItem.cancel: raise DownloadCancelled + + try: + request = get(track.downloadUrl, headers=dz.http_headers, stream=True, timeout=30) + except ConnectionError: + sleep(2) + return stream_track(dz, track, stream, trackAPI, queueItem, interface) + request.raise_for_status() + blowfish_key = str.encode(dz._get_blowfish_key(str(track.id))) + complete = int(request.headers["Content-Length"]) + chunkLength = 0 + percentage = 0 + i = 0 + for chunk in request.iter_content(2048): + if self.queueItem.cancel: raise DownloadCancelled + if i % 3 == 0 and len(chunk) == 2048: + chunk = Blowfish.new(blowfish_key, Blowfish.MODE_CBC, b"\x00\x01\x02\x03\x04\x05\x06\x07").decrypt(chunk) + stream.write(chunk) + chunkLength += len(chunk) + if 'SINGLE_TRACK' in trackAPI: + percentage = (chunkLength / complete) * 100 + self.downloadPercentage = percentage + else: + chunkProgres = (len(chunk) / complete) / trackAPI['SIZE'] * 100 + self.downloadPercentage += chunkProgres + self.updatePercentage() + i += 1 + + def updatePercentage(self): + if round(self.downloadPercentage) != self.lastPercentage and round(self.downloadPercentage) % 2 == 0: + self.lastPercentage = round(self.downloadPercentage) + self.queueItem.progress = self.lastPercentage + if self.interface: + self.interface.send("updateQueue", {'uuid': self.queueItem.uuid, 'progress': self.lastPercentage}) + + def completeTrackPercentage(self): + if isinstance(self.queueItem, QISingle): + self.downloadPercentage = 100 + else: + self.downloadPercentage += (1 / self.queueItem.size) * 100 + self.updatePercentage() + + def removeTrackPercentage(self): + if isinstance(self.queueItem, QISingle): + self.downloadPercentage = 0 + else: + self.downloadPercentage -= (1 / self.queueItem.size) * 100 + self.updatePercentage() + + def downloadWrapper(self, trackAPI_gw): + track = { + 'id': queueItem.single['SNG_ID'], + 'title': queueItem.single['SNG_TITLE'] + (queueItem.single['VERSION'] if 'VERSION' in queueItem.single and queueItem.single['VERSION'] and not queueItem.single['VERSION'] in queueItem.single['SNG_TITLE'] else ""), + 'mainArtist': {'name': queueItem.single['ART_NAME']} + } + + try: + result = self.download(trackAPI_gw) + except DownloadCancelled: + return None + except DownloadFailed as error: + logger.error(f"[{track['mainArtist']['name']} - {track['title']}] {error.message}") + result = {'error': { + 'message': error.message, + 'errid': error.errid, + 'data': track + }} + except Exception as e: + logger.exception(str(e)) + result = {'error': { + 'message': str(e), + 'data': track + }} + + if 'error' in result: + self.completeTrackPercentage() + self.queueItem.failed += 1 + self.queueItem.errors.append(error.message) + if interface: + error = result['error'] + interface.send("updateQueue", { + 'uuid': self.queueItem.uuid, + 'failed': True, + 'data': error['data'], + 'error': error['message'], + 'errid': error['errid'] if 'errid' in error else None + }) + return result + +class DownloadError(Exception): + """Base class for exceptions in this module.""" + pass + +class DownloadFailed(DownloadError): + def __init__(self, errid): + self.errid = errid + self.message = errorMessages[self.errid] + +class DownloadCancelled(DownloadError): + pass diff --git a/deemix/app/queuemanager.py b/deemix/app/QueueManager.py similarity index 100% rename from deemix/app/queuemanager.py rename to deemix/app/QueueManager.py diff --git a/deemix/app/spotify.py b/deemix/app/SpotifyHelper.py similarity index 100% rename from deemix/app/spotify.py rename to deemix/app/SpotifyHelper.py diff --git a/deemix/app/Track.py b/deemix/app/Track.py new file mode 100644 index 0000000..d7e20cd --- /dev/null +++ b/deemix/app/Track.py @@ -0,0 +1,354 @@ +#!/usr/bin/env python3 +import logging + +from deemix.api.deezer import APIError +from deemix.utils.misc import removeFeatures, andCommaConcat, uniqueArray + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger('deemix') + +class Track: + def __init__(self, dz, settings, trackAPI_gw, trackAPI=None, albumAPI_gw=None, albumAPI=None): + self.parseEssentialData(dz, trackAPI_gw) + + self.title = trackAPI_gw['SNG_TITLE'].strip() + if 'VERSION' in trackAPI_gw and trackAPI_gw['VERSION'] and not trackAPI_gw['VERSION'] in trackAPI_gw['SNG_TITLE']: + track.title += " " + trackAPI_gw['VERSION'].strip() + + self.position = None + if 'POSITION' in trackAPI_gw: + self.position = trackAPI_gw['POSITION'] + + self.localTrack = int(self.id) < 0 + if self.localTrack: + self.parseLocalTrackData(trackAPI_gw) + else: + self.parseData(dz, settings, trackAPI_gw, trackAPI, albumAPI_gw, albumAPI) + + if not 'Main' in self.artist: + self.artist['Main'] = [self.mainArtist['name']] + + # Fix incorrect day month when detectable + if int(self.date['month']) > 12: + monthTemp = self.date['month'] + self.date['month'] = self.date['day'] + self.date['day'] = monthTemp + if int(self.album['date']['month']) > 12: + monthTemp = self.album['date']['month'] + self.album['date']['month'] = self.album['date']['day'] + self.album['date']['day'] = monthTemp + + # Add playlist data if track is in a playlist + self.playlist = None + if "_EXTRA_PLAYLIST" in trackAPI_gw: + self.playlist = {} + if 'dzcdn.net' in trackAPI_gw["_EXTRA_PLAYLIST"]['picture_small']: + self.playlist['picUrl'] = trackAPI_gw["_EXTRA_PLAYLIST"]['picture_small'][:-24] + "/{}x{}-{}".format( + settings['embeddedArtworkSize'], settings['embeddedArtworkSize'], + f'000000-{settings["jpegImageQuality"]}-0-0.jpg') + else: + self.playlist['picUrl'] = trackAPI_gw["_EXTRA_PLAYLIST"]['picture_xl'] + self.playlist['title'] = trackAPI_gw["_EXTRA_PLAYLIST"]['title'] + self.playlist['mainArtist'] = { + 'id': trackAPI_gw["_EXTRA_PLAYLIST"]['various_artist']['id'], + 'name': trackAPI_gw["_EXTRA_PLAYLIST"]['various_artist']['name'], + 'pic': trackAPI_gw["_EXTRA_PLAYLIST"]['various_artist']['picture_small'][ + trackAPI_gw["_EXTRA_PLAYLIST"]['various_artist']['picture_small'].find('artist/') + 7:-24] + } + if settings['albumVariousArtists']: + self.playlist['artist'] = {"Main": [trackAPI_gw["_EXTRA_PLAYLIST"]['various_artist']['name'], ]} + self.playlist['artists'] = [trackAPI_gw["_EXTRA_PLAYLIST"]['various_artist']['name'], ] + else: + self.playlist['artist'] = {"Main": []} + self.playlist['artists'] = [] + self.playlist['trackTotal'] = trackAPI_gw["_EXTRA_PLAYLIST"]['nb_tracks'] + self.playlist['recordType'] = "Compilation" + self.playlist['barcode'] = "" + self.playlist['label'] = "" + self.playlist['explicit'] = trackAPI_gw['_EXTRA_PLAYLIST']['explicit'] + self.playlist['date'] = { + 'day': trackAPI_gw["_EXTRA_PLAYLIST"]["creation_date"][8:10], + 'month': trackAPI_gw["_EXTRA_PLAYLIST"]["creation_date"][5:7], + 'year': trackAPI_gw["_EXTRA_PLAYLIST"]["creation_date"][0:4] + } + self.playlist['discTotal'] = "1" + + self.mainArtistsString = andCommaConcat(self.artist['Main']) + self.featArtistsString = None + if 'Featured' in self.artist: + self.featArtistsString = "feat. "+andCommaConcat(self.artist['Featured']) + + # Bits useful for later + self.searched = False + self.selectedFormat = 0 + self.dateString = None + self.album['picUrl'] = None + self.album['picPath'] = None + self.album['bitrate'] = 0 + self.album['dateString'] = None + + self.artistsString + + def parseEssentialData(self, dz, trackAPI_gw): + self.id = trackAPI_gw['SNG_ID'] + self.duration = trackAPI_gw['DURATION'] + self.MD5 = trackAPI_gw['MD5_ORIGIN'] + self.mediaVersion = trackAPI_gw['MEDIA_VERSION'] + self.fallbackId = "0" + if 'FALLBACK' in trackAPI_gw: + self.fallbackId = trackAPI_gw['FALLBACK']['SNG_ID'] + self.formats = dz.get_track_filesizes(track["id"]) + + def parseLocalTrackData(self, trackAPI_gw): + self.album = { + 'id': "0", + 'title': trackAPI_gw['ALB_TITLE'], + 'pic': "" + } + if 'ALB_PICTURE' in trackAPI_gw: + self.album['pic'] = trackAPI_gw['ALB_PICTURE'] + self.mainArtist = { + 'id': "0", + 'name': trackAPI_gw['ART_NAME'], + 'pic': "" + } + self.artists = [trackAPI_gw['ART_NAME']] + self.artist = { + 'Main': [trackAPI_gw['ART_NAME']] + } + self.date = { + 'day': "00", + 'month': "00", + 'year': "XXXX" + } + # All the missing data + self.ISRC = "" + self.album['artist'] = self.artist + self.album['artists'] = self.artists + self.album['barcode'] = "Unknown" + self.album['date'] = self.date + self.album['discTotal'] = "0" + self.album['explicit'] = False + self.album['genre'] = [] + self.album['label'] = "Unknown" + self.album['mainArtist'] = self.mainArtist + self.album['recordType'] = "Album" + self.album['trackTotal'] = "0" + self.bpm = 0 + self.contributors = {} + self.copyright = "" + self.discNumber = "0" + self.explicit = False + self.lyrics = {} + self.replayGain = "" + self.trackNumber = "0" + + def parseData(self, dz, settings, trackAPI_gw, trackAPI, albumAPI_gw, albumAPI): + self.discNumber = None + if 'DISK_NUMBER' in trackAPI_gw: + self.discNumber = trackAPI_gw['DISK_NUMBER'] + self.explicit = None + if 'EXPLICIT_LYRICS' in trackAPI_gw: + self.explicit = trackAPI_gw['EXPLICIT_LYRICS'] + self.copyright = None + if 'COPYRIGHT' in trackAPI_gw: + self.copyright = trackAPI_gw['COPYRIGHT'] + self.replayGain = "" + if 'GAIN' in trackAPI_gw: + self.replayGain = "{0:.2f} dB".format((float(trackAPI_gw['GAIN']) + 18.4) * -1) + self.ISRC = trackAPI_gw['ISRC'] + self.trackNumber = trackAPI_gw['TRACK_NUMBER'] + self.contributors = trackAPI_gw['SNG_CONTRIBUTORS'] + + track.lyrics = { + 'id': None, + 'unsync': None, + 'sync': None + } + if 'LYRICS_ID' in trackAPI_gw: + self.lyrics['id'] = trackAPI_gw['LYRICS_ID'] + if not "LYRICS" in trackAPI_gw and int(self.lyrics['id']) != 0: + logger.info(f"[{trackAPI_gw['ART_NAME']} - {self.title}] Getting lyrics") + trackAPI_gw["LYRICS"] = dz.get_lyrics_gw(self.id) + if int(self.lyrics['id']) != 0: + if "LYRICS_TEXT" in trackAPI_gw["LYRICS"]: + self.lyrics['unsync'] = trackAPI_gw["LYRICS"]["LYRICS_TEXT"] + if "LYRICS_SYNC_JSON" in trackAPI_gw["LYRICS"]: + self.lyrics['sync'] = "" + lastTimestamp = "" + for i in range(len(trackAPI_gw["LYRICS"]["LYRICS_SYNC_JSON"])): + if "lrc_timestamp" in trackAPI_gw["LYRICS"]["LYRICS_SYNC_JSON"][i]: + self.lyrics['sync'] += trackAPI_gw["LYRICS"]["LYRICS_SYNC_JSON"][i]["lrc_timestamp"] + lastTimestamp = trackAPI_gw["LYRICS"]["LYRICS_SYNC_JSON"][i]["lrc_timestamp"] + else: + self.lyrics['sync'] += lastTimestamp + self.lyrics['sync'] += trackAPI_gw["LYRICS"]["LYRICS_SYNC_JSON"][i]["line"] + "\r\n" + + track.mainArtist = { + 'id': trackAPI_gw['ART_ID'], + 'name': trackAPI_gw['ART_NAME'], + 'pic': None + } + if 'ART_PICTURE' in trackAPI_gw: + track.mainArtist['pic'] = trackAPI_gw['ART_PICTURE'] + + self.date = None + if 'PHYSICAL_RELEASE_DATE' in trackAPI_gw: + self.date = { + 'day': trackAPI_gw["PHYSICAL_RELEASE_DATE"][8:10], + 'month': trackAPI_gw["PHYSICAL_RELEASE_DATE"][5:7], + 'year': trackAPI_gw["PHYSICAL_RELEASE_DATE"][0:4] + } + + self.album = { + 'id': trackAPI_gw['ALB_ID'], + 'title': trackAPI_gw['ALB_TITLE'], + 'pic': None, + 'barcode': "Unknown", + 'label': "Unknown", + 'explicit': False, + 'date': None, + 'genre': [] + } + if 'ALB_PICTURE' in trackAPI_gw: + self.album['pic'] = trackAPI_gw['ALB_PICTURE'] + try: + # Try the public API first (as it has more data) + if not albumAPI: + logger.info(f"[{self.mainArtist['name']} - {self.title}] Getting album infos") + albumAPI = dz.get_album(self.album['id']) + self.album['title'] = albumAPI['title'] + self.album['mainArtist'] = { + 'id': albumAPI['artist']['id'], + 'name': albumAPI['artist']['name'], + 'pic': albumAPI['artist']['picture_small'][albumAPI['artist']['picture_small'].find('artist/') + 7:-24] + } + + self.album['artist'] = {} + self.album['artists'] = [] + for artist in albumAPI['contributors']: + if artist['id'] != 5080 or artist['id'] == 5080 and settings['albumVariousArtists']: + if artist['name'] not in self.album['artists']: + self.album['artists'].append(artist['name']) + if artist['role'] == "Main" or artist['role'] != "Main" and artist['name'] not in self.album['artist']['Main']: + if not artist['role'] in self.album['artist']: + self.album['artist'][artist['role']] = [] + self.album['artist'][artist['role']].append(artist['name']) + if settings['removeDuplicateArtists']: + self.album['artists'] = uniqueArray(self.album['artists']) + for role in self.album['artist'].keys(): + self.album['artist'][role] = uniqueArray(self.album['artist'][role]) + + self.album['trackTotal'] = albumAPI['nb_tracks'] + self.album['recordType'] = albumAPI['record_type'] + + if 'upc' in albumAPI: + self.album['barcode'] = albumAPI['upc'] + if 'label' in albumAPI: + self.album['label'] = albumAPI['label'] + if 'explicit_lyrics' in albumAPI: + self.album['explicit'] = albumAPI['explicit_lyrics'] + if 'release_date' in albumAPI: + self.album['date'] = { + 'day': albumAPI["release_date"][8:10], + 'month': albumAPI["release_date"][5:7], + 'year': albumAPI["release_date"][0:4] + } + self.album['discTotal'] = None + if 'nb_disk' in albumAPI: + self.album['discTotal'] = albumAPI['nb_disk'] + self.copyright = None + if 'copyright' in albumAPI: + self.copyright = albumAPI['copyright'] + + if not track.album['pic']: + self.album['pic'] = albumAPI['cover_small'][albumAPI['cover_small'].find('cover/') + 6:-24] + + if 'genres' in albumAPI and 'data' in albumAPI['genres'] and len(albumAPI['genres']['data']) > 0: + for genre in albumAPI['genres']['data']: + self.album['genre'].append(genre['name']) + except APIError: + if not albumAPI_gw: + logger.info(f"[{self.mainArtist['name']} - {self.title}] Getting more album infos") + albumAPI_gw = dz.get_album_gw(self.album['id']) + self.album['title'] = albumAPI_gw['ALB_TITLE'] + self.album['mainArtist'] = { + 'id': albumAPI_gw['ART_ID'], + 'name': albumAPI_gw['ART_NAME'], + 'pic': None + } + logger.info(f"[{self.mainArtist['name']} - {self.title}] Getting artist picture fallback") + artistAPI = dz.get_artist(self.album['mainArtist']['id']) + self.album['artists'] = [albumAPI_gw['ART_NAME']] + self.album['mainArtist']['pic'] = artistAPI['picture_small'][artistAPI['picture_small'].find('artist/') + 7:-24] + self.album['trackTotal'] = albumAPI_gw['NUMBER_TRACK'] + self.album['discTotal'] = albumAPI_gw['NUMBER_DISK'] + self.album['recordType'] = "Album" + if 'LABEL_NAME' in albumAPI_gw: + self.album['label'] = albumAPI_gw['LABEL_NAME'] + if 'EXPLICIT_ALBUM_CONTENT' in albumAPI_gw and 'EXPLICIT_LYRICS_STATUS' in albumAPI_gw['EXPLICIT_ALBUM_CONTENT']: + self.album['explicit'] = albumAPI_gw['EXPLICIT_ALBUM_CONTENT']['EXPLICIT_LYRICS_STATUS'] in [1,4] + if not self.album['pic']: + self.album['pic'] = albumAPI_gw['ALB_PICTURE'] + if 'PHYSICAL_RELEASE_DATE' in albumAPI_gw: + self.album['date'] = { + 'day': albumAPI_gw["PHYSICAL_RELEASE_DATE"][8:10], + 'month': albumAPI_gw["PHYSICAL_RELEASE_DATE"][5:7], + 'year': albumAPI_gw["PHYSICAL_RELEASE_DATE"][0:4] + } + + self.album['mainArtist']['save'] = self.album['mainArtist']['id'] != 5080 or self.album['mainArtist']['id'] == 5080 and settings['albumVariousArtists'] + + if self.album['date'] and not self.date: + self.date = self.album['date'] + + if not trackAPI: + logger.info(f"[{self.mainArtist['name']} - {self.title}] Getting extra track infos") + trackAPI = dz.get_track(self.id) + self.bpm = trackAPI['bpm'] + + if not self.replayGain and 'gain' in trackAPI: + self.replayGain = "{0:.2f} dB".format((float(trackAPI['gain']) + 18.4) * -1) + if not self.explicit: + self.explicit = trackAPI['explicit_lyrics'] + if not self.discNumber: + self.discNumber = trackAPI['disk_number'] + + self.artist = {} + self.artists = [] + for artist in trackAPI['contributors']: + if artist['id'] != 5080 or artist['id'] == 5080 and len(trackAPI['contributors']) == 1: + if artist['name'] not in self.artists: + self.artists.append(artist['name']) + if artist['role'] != "Main" and artist['name'] not in self.artist['Main'] or artist['role'] == "Main": + if not artist['role'] in self.artist: + self.artist[artist['role']] = [] + self.artist[artist['role']].append(artist['name']) + if settings['removeDuplicateArtists']: + self.artists = uniqueArray(self.artists) + for role in self.artist.keys(): + self.artist[role] = uniqueArray(self.artist[role]) + + if not self.album['discTotal']: + if not albumAPI_gw: + logger.info(f"[{self.mainArtist['name']} - {self.title}] Getting more album infos") + albumAPI_gw = dz.get_album_gw(self.album['id']) + self.album['discTotal'] = albumAPI_gw['NUMBER_DISK'] + if not self.copyright: + if not albumAPI_gw: + logger.info(f"[{self.mainArtist['name']} - {self.title}] Getting more album infos") + albumAPI_gw = dz.get_album_gw(self.album['id']) + self.copyright = albumAPI_gw['COPYRIGHT'] + + # Removes featuring from the title + def getCleanTitle(self): + return removeFeatures(self.title) + + # Removes featuring from the album name + def getCleanAlbumTitle(self): + return removeFeatures(self.album['title']) + + def getFeatTitle(self): + if self.featArtistsString and not "(feat." in self.title.lower(): + return self.title + " ({})".format(self.featArtistsString) + return self.title diff --git a/deemix/app/downloader.py b/deemix/app/downloader.py deleted file mode 100644 index 4b91cb3..0000000 --- a/deemix/app/downloader.py +++ /dev/null @@ -1,1075 +0,0 @@ -#!/usr/bin/env python3 -import os.path -import re -import traceback -from concurrent.futures import ThreadPoolExecutor -from os import makedirs, remove, system as execute -from tempfile import gettempdir -from time import sleep - -from Cryptodome.Cipher import Blowfish -from requests import get -from requests.exceptions import HTTPError, ConnectionError - -from deemix.api.deezer import APIError, USER_AGENT_HEADER -from deemix.utils.misc import changeCase, uniqueArray -from deemix.utils.pathtemplates import generateFilename, generateFilepath, settingsRegexAlbum, settingsRegexArtist, settingsRegexPlaylistFile -from deemix.utils.taggers import tagID3, tagFLAC -from deemix.app.queueitem import QISingle, QICollection, QIConvertable -from mutagen.flac import FLACNoHeaderError -import logging - -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger('deemix') - -TEMPDIR = os.path.join(gettempdir(), 'deemix-imgs') -if not os.path.isdir(TEMPDIR): - makedirs(TEMPDIR) - -extensions = { - 9: '.flac', - 0: '.mp3', - 3: '.mp3', - 1: '.mp3', - 8: '.mp3', - 15: '.mp4', - 14: '.mp4', - 13: '.mp4' -} -downloadPercentage = 0 -lastPercentage = 0 - - -def stream_track(dz, track, stream, trackAPI, queueItem, interface=None): - global downloadPercentage, lastPercentage - if queueItem.cancel: - raise downloadCancelled - try: - request = get(track['downloadUrl'], headers=dz.http_headers, stream=True, timeout=30) - except ConnectionError: - sleep(2) - return stream_track(dz, track, stream, trackAPI, queueItem, interface) - request.raise_for_status() - blowfish_key = str.encode(dz._get_blowfish_key(str(track['id']))) - complete = int(request.headers["Content-Length"]) - chunkLength = 0 - percentage = 0 - i = 0 - for chunk in request.iter_content(2048): - if queueItem.cancel: - raise downloadCancelled - if i % 3 == 0 and len(chunk) == 2048: - chunk = Blowfish.new(blowfish_key, Blowfish.MODE_CBC, b"\x00\x01\x02\x03\x04\x05\x06\x07").decrypt(chunk) - stream.write(chunk) - chunkLength += len(chunk) - if 'SINGLE_TRACK' in trackAPI: - percentage = (chunkLength / complete) * 100 - downloadPercentage = percentage - else: - chunkProgres = (len(chunk) / complete) / trackAPI['SIZE'] * 100 - downloadPercentage += chunkProgres - if round(downloadPercentage) != lastPercentage and round(downloadPercentage) % 2 == 0: - lastPercentage = round(downloadPercentage) - queueItem.progress = lastPercentage - if interface: - interface.send("updateQueue", {'uuid': queueItem.uuid, 'progress': lastPercentage}) - i += 1 - - -def trackCompletePercentage(trackAPI, queueItem, interface): - global downloadPercentage, lastPercentage - if 'SINGLE_TRACK' in trackAPI: - downloadPercentage = 100 - else: - downloadPercentage += 1 / trackAPI['SIZE'] * 100 - if round(downloadPercentage) != lastPercentage and round(downloadPercentage) % 2 == 0: - lastPercentage = round(downloadPercentage) - queueItem.progress = lastPercentage - if interface: - interface.send("updateQueue", {'uuid': queueItem.uuid, 'progress': lastPercentage}) - -def trackRemovePercentage(trackAPI, queueItem, interface): - global downloadPercentage, lastPercentage - if 'SINGLE_TRACK' in trackAPI: - downloadPercentage = 0 - else: - downloadPercentage -= 1 / trackAPI['SIZE'] * 100 - if round(downloadPercentage) != lastPercentage and round(downloadPercentage) % 2 == 0: - lastPercentage = round(downloadPercentage) - queueItem.progress = lastPercentage - if interface: - interface.send("updateQueue", {'uuid': queueItem.uuid, 'progress': lastPercentage}) - - -def downloadImage(url, path, overwrite="n"): - if not os.path.isfile(path) or overwrite in ['y', 't', 'b']: - 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 HTTPError: - if 'cdns-images.dzcdn.net' in url: - urlBase = url[:url.rfind("/")+1] - pictureUrl = url[len(urlBase):] - pictureSize = int(pictureUrl[:pictureUrl.find("x")]) - if pictureSize > 1400: - logger.warn("Couldn't download "+str(pictureSize)+"x"+str(pictureSize)+" image, falling back to 1400x1400") - sleep(1) - return downloadImage(urlBase+pictureUrl.replace(str(pictureSize)+"x"+str(pictureSize), '1400x1400'), path, overwrite) - logger.error("Couldn't download Image: "+url) - except: - sleep(1) - return downloadImage(url, path, overwrite) - remove(path) - return None - else: - return path - - -def formatDate(date, template): - elements = { - 'year': ['YYYY', 'YY', 'Y'], - 'month': ['MM', 'M'], - 'day': ['DD', 'D'] - } - for element, placeholders in elements.items(): - for placeholder in placeholders: - if placeholder in template: - template = template.replace(placeholder, str(date[element])) - return template - - -def getPreferredBitrate(dz, track, bitrate, fallback=True): - if 'localTrack' in track: - return 0 - - formats_non_360 = { - 9: "FLAC", - 3: "MP3_320", - 1: "MP3_128", - } - formats_360 = { - 15: "MP4_RA3", - 14: "MP4_RA2", - 13: "MP4_RA1", - } - - if not fallback: - error_num = -100 - formats = formats_360 - formats.update(formats_non_360) - elif int(bitrate) in formats_360: - error_num = -200 - formats = formats_360 - else: - error_num = 8 - formats = formats_non_360 - - filesizes = dz.get_track_filesizes(track["id"]) - - for format_num, format in formats.items(): - if format_num <= int(bitrate): - if f"FILESIZE_{format}" in filesizes and int(filesizes[f"FILESIZE_{format}"]) != 0 and ((format_num == 9 and not 'flacCorrupted' in track) or format_num != 9): - return format_num - else: - if fallback: - continue - else: - return error_num - - return error_num # fallback is enabled and loop went through all formats - - -def parseEssentialTrackData(track, trackAPI): - track['id'] = trackAPI['SNG_ID'] - track['duration'] = trackAPI['DURATION'] - track['MD5'] = trackAPI['MD5_ORIGIN'] - track['mediaVersion'] = trackAPI['MEDIA_VERSION'] - if 'FALLBACK' in trackAPI: - track['fallbackId'] = trackAPI['FALLBACK']['SNG_ID'] - else: - track['fallbackId'] = 0 - return track - - -def getTrackData(dz, trackAPI_gw, settings, trackAPI=None, albumAPI_gw=None, albumAPI=None): - track = {} - track['title'] = trackAPI_gw['SNG_TITLE'].strip() - if 'VERSION' in trackAPI_gw and trackAPI_gw['VERSION'] and not trackAPI_gw['VERSION'] in trackAPI_gw['SNG_TITLE']: - track['title'] += " " + trackAPI_gw['VERSION'].strip() - - track = parseEssentialTrackData(track, trackAPI_gw) - - if int(track['id']) < 0: - track['album'] = {} - track['album']['id'] = 0 - track['album']['title'] = trackAPI_gw['ALB_TITLE'] - if 'ALB_PICTURE' in trackAPI_gw: - track['album']['pic'] = trackAPI_gw['ALB_PICTURE'] - track['mainArtist'] = {} - track['mainArtist']['id'] = 0 - track['mainArtist']['name'] = trackAPI_gw['ART_NAME'] - track['mainArtist']['pic'] = "" - track['artists'] = [trackAPI_gw['ART_NAME']] - track['artist'] = { - 'Main': [trackAPI_gw['ART_NAME']] - } - track['date'] = { - 'day': "XXXX", - 'month': "00", - 'year': "00" - } - if 'POSITION' in trackAPI_gw: track['position'] = trackAPI_gw['POSITION'] - track['localTrack'] = True - # Missing tags - track['ISRC'] = "" - track['album']['artist'] = track['artist'] - track['album']['artists'] = track['artists'] - track['album']['barcode'] = "Unknown" - track['album']['date'] = track['date'] - track['album']['discTotal'] = "0" - track['album']['explicit'] = False - track['album']['genre'] = [] - track['album']['label'] = "Unknown" - track['album']['mainArtist'] = track['mainArtist'] - track['album']['recordType'] = "Album" - track['album']['trackTotal'] = "0" - track['bpm'] = 0 - track['contributors'] = {} - track['copyright'] = "" - track['discNumber'] = "0" - track['explicit'] = False - track['lyrics'] = {} - track['replayGain'] = "" - track['trackNumber'] = "0" - else: - if 'DISK_NUMBER' in trackAPI_gw: - track['discNumber'] = trackAPI_gw['DISK_NUMBER'] - if 'EXPLICIT_LYRICS' in trackAPI_gw: - track['explicit'] = trackAPI_gw['EXPLICIT_LYRICS'] != "0" - if 'COPYRIGHT' in trackAPI_gw: - track['copyright'] = trackAPI_gw['COPYRIGHT'] - track['replayGain'] = "{0:.2f} dB".format( - (float(trackAPI_gw['GAIN']) + 18.4) * -1) if 'GAIN' in trackAPI_gw else None - track['ISRC'] = trackAPI_gw['ISRC'] - track['trackNumber'] = trackAPI_gw['TRACK_NUMBER'] - track['contributors'] = trackAPI_gw['SNG_CONTRIBUTORS'] - if 'POSITION' in trackAPI_gw: - track['position'] = trackAPI_gw['POSITION'] - - track['lyrics'] = {} - if 'LYRICS_ID' in trackAPI_gw: - track['lyrics']['id'] = trackAPI_gw['LYRICS_ID'] - if not "LYRICS" in trackAPI_gw and int(track['lyrics']['id']) != 0: - logger.info(f"[{trackAPI_gw['ART_NAME']} - {track['title']}] Getting lyrics") - trackAPI_gw["LYRICS"] = dz.get_lyrics_gw(track['id']) - if int(track['lyrics']['id']) != 0: - if "LYRICS_TEXT" in trackAPI_gw["LYRICS"]: - track['lyrics']['unsync'] = trackAPI_gw["LYRICS"]["LYRICS_TEXT"] - if "LYRICS_SYNC_JSON" in trackAPI_gw["LYRICS"]: - track['lyrics']['sync'] = "" - lastTimestamp = "" - for i in range(len(trackAPI_gw["LYRICS"]["LYRICS_SYNC_JSON"])): - if "lrc_timestamp" in trackAPI_gw["LYRICS"]["LYRICS_SYNC_JSON"][i]: - track['lyrics']['sync'] += trackAPI_gw["LYRICS"]["LYRICS_SYNC_JSON"][i]["lrc_timestamp"] - lastTimestamp = trackAPI_gw["LYRICS"]["LYRICS_SYNC_JSON"][i]["lrc_timestamp"] - else: - track['lyrics']['sync'] += lastTimestamp - track['lyrics']['sync'] += trackAPI_gw["LYRICS"]["LYRICS_SYNC_JSON"][i]["line"] + "\r\n" - - track['mainArtist'] = {} - track['mainArtist']['id'] = trackAPI_gw['ART_ID'] - track['mainArtist']['name'] = trackAPI_gw['ART_NAME'] - if 'ART_PICTURE' in trackAPI_gw: - track['mainArtist']['pic'] = trackAPI_gw['ART_PICTURE'] - - if 'PHYSICAL_RELEASE_DATE' in trackAPI_gw: - track['date'] = { - 'day': trackAPI_gw["PHYSICAL_RELEASE_DATE"][8:10], - 'month': trackAPI_gw["PHYSICAL_RELEASE_DATE"][5:7], - 'year': trackAPI_gw["PHYSICAL_RELEASE_DATE"][0:4] - } - - track['album'] = {} - track['album']['id'] = trackAPI_gw['ALB_ID'] - track['album']['title'] = trackAPI_gw['ALB_TITLE'] - if 'ALB_PICTURE' in trackAPI_gw: - track['album']['pic'] = trackAPI_gw['ALB_PICTURE'] - - try: - if not albumAPI: - logger.info(f"[{track['mainArtist']['name']} - {track['title']}] Getting album infos") - albumAPI = dz.get_album(track['album']['id']) - track['album']['title'] = albumAPI['title'] - track['album']['mainArtist'] = { - 'id': albumAPI['artist']['id'], - 'name': albumAPI['artist']['name'], - 'pic': albumAPI['artist']['picture_small'][albumAPI['artist']['picture_small'].find('artist/') + 7:-24] - } - track['album']['artist'] = {} - track['album']['artists'] = [] - for artist in albumAPI['contributors']: - if artist['id'] != 5080 or artist['id'] == 5080 and settings['albumVariousArtists']: - if artist['name'] not in track['album']['artists']: - track['album']['artists'].append(artist['name']) - if artist['role'] != "Main" and artist['name'] not in track['album']['artist']['Main'] or artist['role'] == "Main": - if not artist['role'] in track['album']['artist']: - track['album']['artist'][artist['role']] = [] - track['album']['artist'][artist['role']].append(artist['name']) - if settings['removeDuplicateArtists']: - track['album']['artists'] = uniqueArray(track['album']['artists']) - for role in track['album']['artist'].keys(): - track['album']['artist'][role] = uniqueArray(track['album']['artist'][role]) - track['album']['trackTotal'] = albumAPI['nb_tracks'] - track['album']['recordType'] = albumAPI['record_type'] - track['album']['barcode'] = albumAPI['upc'] if 'upc' in albumAPI else "Unknown" - track['album']['label'] = albumAPI['label'] if 'label' in albumAPI else "Unknown" - track['album']['explicit'] = albumAPI['explicit_lyrics'] if 'explicit_lyrics' in albumAPI else False - if not 'pic' in track['album']: - track['album']['pic'] = albumAPI['cover_small'][albumAPI['cover_small'].find('cover/') + 6:-24] - if 'release_date' in albumAPI: - track['album']['date'] = { - 'day': albumAPI["release_date"][8:10], - 'month': albumAPI["release_date"][5:7], - 'year': albumAPI["release_date"][0:4] - } - track['album']['discTotal'] = albumAPI['nb_disk'] if 'nb_disk' in albumAPI else None - track['copyright'] = albumAPI['copyright'] if 'copyright' in albumAPI else None - track['album']['genre'] = [] - if 'genres' in albumAPI and 'data' in albumAPI['genres'] and len(albumAPI['genres']['data']) > 0: - for genre in albumAPI['genres']['data']: - track['album']['genre'].append(genre['name']) - except APIError: - if not albumAPI_gw: - logger.info(f"[{track['mainArtist']['name']} - {track['title']}] Getting more album infos") - albumAPI_gw = dz.get_album_gw(track['album']['id']) - track['album']['title'] = albumAPI_gw['ALB_TITLE'] - track['album']['mainArtist'] = { - 'id': albumAPI_gw['ART_ID'], - 'name': albumAPI_gw['ART_NAME'] - } - logger.info(f"[{track['mainArtist']['name']} - {track['title']}] Getting artist picture fallback") - artistAPI = dz.get_artist(track['album']['mainArtist']['id']) - track['album']['artists'] = albumAPI_gw['ART_NAME'] - track['album']['mainArtist']['pic'] = artistAPI['picture_small'][ - artistAPI['picture_small'].find('artist/') + 7:-24] - track['album']['trackTotal'] = albumAPI_gw['NUMBER_TRACK'] - track['album']['discTotal'] = albumAPI_gw['NUMBER_DISK'] - track['album']['recordType'] = "Album" - track['album']['barcode'] = "Unknown" - track['album']['label'] = albumAPI_gw['LABEL_NAME'] if 'LABEL_NAME' in albumAPI_gw else "Unknown" - track['album']['explicit'] = albumAPI_gw['EXPLICIT_ALBUM_CONTENT']['EXPLICIT_LYRICS_STATUS'] in [1,4] if 'EXPLICIT_ALBUM_CONTENT' in albumAPI_gw and 'EXPLICIT_LYRICS_STATUS' in albumAPI_gw['EXPLICIT_ALBUM_CONTENT'] else False - if not 'pic' in track['album']: - track['album']['pic'] = albumAPI_gw['ALB_PICTURE'] - if 'PHYSICAL_RELEASE_DATE' in albumAPI_gw: - track['album']['date'] = { - 'day': albumAPI_gw["PHYSICAL_RELEASE_DATE"][8:10], - 'month': albumAPI_gw["PHYSICAL_RELEASE_DATE"][5:7], - 'year': albumAPI_gw["PHYSICAL_RELEASE_DATE"][0:4] - } - track['album']['genre'] = [] - track['album']['mainArtist']['save'] = track['album']['mainArtist']['id'] != 5080 or track['album']['mainArtist']['id'] == 5080 and settings['albumVariousArtists'] - - if 'date' in track['album'] and 'date' not in track: - track['date'] = track['album']['date'] - - if not trackAPI: - logger.info(f"[{track['mainArtist']['name']} - {track['title']}] Getting extra track infos") - trackAPI = dz.get_track(track['id']) - track['bpm'] = trackAPI['bpm'] - if not 'replayGain' in track or not track['replayGain']: - track['replayGain'] = "{0:.2f} dB".format((float(trackAPI['gain']) + 18.4) * -1) if 'gain' in trackAPI else "" - if not 'explicit' in track: - track['explicit'] = trackAPI['explicit_lyrics'] - if not 'discNumber' in track: - track['discNumber'] = trackAPI['disk_number'] - track['artist'] = {} - track['artists'] = [] - for artist in trackAPI['contributors']: - if artist['id'] != 5080 or artist['id'] == 5080 and len(trackAPI['contributors']) == 1: - if artist['name'] not in track['artists']: - track['artists'].append(artist['name']) - if artist['role'] != "Main" and artist['name'] not in track['artist']['Main'] or artist['role'] == "Main": - if not artist['role'] in track['artist']: - track['artist'][artist['role']] = [] - track['artist'][artist['role']].append(artist['name']) - if settings['removeDuplicateArtists']: - track['artists'] = uniqueArray(track['artists']) - for role in track['artist'].keys(): - track['artist'][role] = uniqueArray(track['artist'][role]) - - if not 'discTotal' in track['album'] or not track['album']['discTotal']: - if not albumAPI_gw: - logger.info(f"[{track['mainArtist']['name']} - {track['title']}] Getting more album infos") - albumAPI_gw = dz.get_album_gw(track['album']['id']) - track['album']['discTotal'] = albumAPI_gw['NUMBER_DISK'] - if not 'copyright' in track or not track['copyright']: - if not albumAPI_gw: - logger.info(f"[{track['mainArtist']['name']} - {track['title']}] Getting more album infos") - albumAPI_gw = dz.get_album_gw(track['album']['id']) - track['copyright'] = albumAPI_gw['COPYRIGHT'] - - # Fix incorrect day month when detectable - if int(track['date']['month']) > 12: - monthTemp = track['date']['month'] - track['date']['month'] = track['date']['day'] - track['date']['day'] = monthTemp - if int(track['album']['date']['month']) > 12: - monthTemp = track['album']['date']['month'] - track['album']['date']['month'] = track['album']['date']['day'] - track['album']['date']['day'] = monthTemp - - # Remove featuring from the title - track['title_clean'] = track['title'] - if "(feat." in track['title_clean'].lower(): - pos = track['title_clean'].lower().find("(feat.") - tempTrack = track['title_clean'][:pos] - if ")" in track['title_clean']: - tempTrack += track['title_clean'][track['title_clean'].find(")", pos + 1) + 1:] - track['title_clean'] = tempTrack.strip() - - # Remove featuring from the album name - track['album']['title_clean'] = track['album']['title'] - if "(feat." in track['album']['title_clean'].lower(): - pos = track['album']['title_clean'].lower().find("(feat.") - tempTrack = track['album']['title_clean'][:pos] - if ")" in track['album']['title_clean']: - tempTrack += track['album']['title_clean'][track['album']['title_clean'].find(")", pos + 1) + 1:] - track['album']['title_clean'] = tempTrack.strip() - - # Create artists strings - track['mainArtistsString'] = "" - track['commaArtistsString'] = "" - if 'Main' in track['artist']: - tot = len(track['artist']['Main']) - for i, art in enumerate(track['artist']['Main']): - track['mainArtistsString'] += art - track['commaArtistsString'] += art - if tot != i + 1: - track['commaArtistsString'] += ", " - if tot - 1 == i + 1: - track['mainArtistsString'] += " & " - else: - track['mainArtistsString'] += ", " - else: - track['mainArtistsString'] = track['mainArtist']['name'] - track['commaArtistsString'] = track['mainArtist']['name'] - if 'Featured' in track['artist']: - tot = len(track['artist']['Featured']) - track['featArtistsString'] = "feat. " - for i, art in enumerate(track['artist']['Featured']): - track['featArtistsString'] += art - if tot != i + 1: - if tot - 1 == i + 1: - track['featArtistsString'] += " & " - else: - track['featArtistsString'] += ", " - - # Create title with feat - if "(feat." in track['title'].lower(): - track['title_feat'] = track['title'] - elif 'Featured' in track['artist']: - track['title_feat'] = track['title'] + " ({})".format(track['featArtistsString']) - else: - track['title_feat'] = track['title'] - - if "_EXTRA_PLAYLIST" in trackAPI: - track['playlist'] = {} - if 'dzcdn.net' in trackAPI["_EXTRA_PLAYLIST"]['picture_small']: - track['playlist']['picUrl'] = trackAPI["_EXTRA_PLAYLIST"]['picture_small'][:-24] + "/{}x{}-{}".format( - settings['embeddedArtworkSize'], settings['embeddedArtworkSize'], - f'000000-{settings["jpegImageQuality"]}-0-0.jpg') - else: - track['playlist']['picUrl'] = trackAPI["_EXTRA_PLAYLIST"]['picture_xl'] - track['playlist']['title'] = trackAPI["_EXTRA_PLAYLIST"]['title'] - track['playlist']['mainArtist'] = { - 'id': trackAPI["_EXTRA_PLAYLIST"]['various_artist']['id'], - 'name': trackAPI["_EXTRA_PLAYLIST"]['various_artist']['name'], - 'pic': trackAPI["_EXTRA_PLAYLIST"]['various_artist']['picture_small'][ - trackAPI["_EXTRA_PLAYLIST"]['various_artist']['picture_small'].find('artist/') + 7:-24] - } - if settings['albumVariousArtists']: - track['playlist']['artist'] = {"Main": [trackAPI["_EXTRA_PLAYLIST"]['various_artist']['name'], ]} - track['playlist']['artists'] = [trackAPI["_EXTRA_PLAYLIST"]['various_artist']['name'], ] - else: - track['playlist']['artist'] = {"Main": []} - track['playlist']['artists'] = [] - track['playlist']['trackTotal'] = trackAPI["_EXTRA_PLAYLIST"]['nb_tracks'] - track['playlist']['recordType'] = "Compilation" - track['playlist']['barcode'] = "" - track['playlist']['label'] = "" - track['playlist']['explicit'] = trackAPI['_EXTRA_PLAYLIST']['explicit'] - track['playlist']['date'] = { - 'day': trackAPI["_EXTRA_PLAYLIST"]["creation_date"][8:10], - 'month': trackAPI["_EXTRA_PLAYLIST"]["creation_date"][5:7], - 'year': trackAPI["_EXTRA_PLAYLIST"]["creation_date"][0:4] - } - track['playlist']['discTotal'] = "1" - - return track - - -def downloadTrackObj(dz, trackAPI, settings, bitrate, queueItem, extraTrack=None, interface=None): - result = {} - if queueItem.cancel: - result['cancel'] = True - return result - - if trackAPI['SNG_ID'] == 0: - result['error'] = { - 'message': "Track not available on Deezer!", - 'errid': 'notOnDeezer' - } - if 'SNG_TITLE' in trackAPI: - result['error']['data'] = { - 'id': trackAPI['SNG_ID'], - 'title': trackAPI['SNG_TITLE'] + (trackAPI['VERSION'] if 'VERSION' in trackAPI and trackAPI['VERSION'] and not trackAPI['VERSION'] in trackAPI['SNG_TITLE'] else ""), - 'artist': trackAPI['ART_NAME'] - } - logger.error(f"[{result['error']['data']['artist']} - {result['error']['data']['title']}] This track is not available on Deezer!") - queueItem.failed += 1 - queueItem.errors.append(result['error']) - if interface: - interface.send("updateQueue", {'uuid': queueItem.uuid, 'failed': True, 'data': result['error']['data'], - 'error': result['error']['message'], 'errid': result['error']['errid']}) - return result - # Get the metadata - logger.info(f"[{trackAPI['ART_NAME']} - {trackAPI['SNG_TITLE']}] Getting the tags") - if extraTrack: - track = extraTrack - else: - track = getTrackData(dz, - trackAPI_gw=trackAPI, - settings=settings, - trackAPI=trackAPI['_EXTRA_TRACK'] if '_EXTRA_TRACK' in trackAPI else None, - albumAPI=trackAPI['_EXTRA_ALBUM'] if '_EXTRA_ALBUM' in trackAPI else None - ) - if queueItem.cancel: - result['cancel'] = True - return result - if track['MD5'] == '': - if track['fallbackId'] != 0: - logger.warn(f"[{track['mainArtist']['name']} - {track['title']}] Track not yet encoded, using fallback id") - trackNew = dz.get_track_gw(track['fallbackId']) - track = parseEssentialTrackData(track, trackNew) - if 'flacCorrupted' in track: del track['flacCorrupted'] - return downloadTrackObj(dz, trackAPI, settings, bitrate, queueItem, extraTrack=track, interface=interface) - elif not 'searched' in track and settings['fallbackSearch']: - logger.warn(f"[{track['mainArtist']['name']} - {track['title']}] Track not yet encoded, searching for alternative") - searchedId = dz.get_track_from_metadata(track['mainArtist']['name'], track['title'], - track['album']['title']) - if searchedId != 0: - trackNew = dz.get_track_gw(searchedId) - track = parseEssentialTrackData(track, trackNew) - if 'flacCorrupted' in track: del track['flacCorrupted'] - track['searched'] = True - return downloadTrackObj(dz, trackAPI, settings, bitrate, queueItem, extraTrack=track, - interface=interface) - else: - logger.error(f"[{track['mainArtist']['name']} - {track['title']}] Track not yet encoded and no alternative found!") - trackCompletePercentage(trackAPI, queueItem, interface) - result['error'] = { - 'message': "Track not yet encoded and no alternative found!", - 'errid': 'notEncodedNoAlternative', - 'data': { - 'id': track['id'], - 'title': track['title'], - 'artist': track['mainArtist']['name'] - } - } - queueItem.failed += 1 - queueItem.errors.append(result['error']) - if interface: - interface.send("updateQueue", {'uuid': queueItem.uuid, 'failed': True, 'data': result['error']['data'], - 'error': result['error']['message'], 'errid': result['error']['errid']}) - return result - else: - logger.error(f"[{track['mainArtist']['name']} - {track['title']}] Track not yet encoded!") - trackCompletePercentage(trackAPI, queueItem, interface) - result['error'] = { - 'message': "Track not yet encoded!", - 'errid': 'notEncoded', - 'data': { - 'id': track['id'], - 'title': track['title'], - 'artist': track['mainArtist']['name'] - } - } - queueItem.failed += 1 - queueItem.errors.append(result['error']) - if interface: - interface.send("updateQueue", {'uuid': queueItem.uuid, 'failed': True, 'data': result['error']['data'], - 'error': result['error']['message'], 'errid': result['error']['errid']}) - return result - - # Get the selected bitrate - selectedBitrate = getPreferredBitrate(dz, track, bitrate, settings['fallbackBitrate']) - if selectedBitrate == -100: - if track['fallbackId'] != 0: - logger.warn(f"[{track['mainArtist']['name']} - {track['title']}] Track not found at desired bitrate, using fallback id") - trackNew = dz.get_track_gw(track['fallbackId']) - track = parseEssentialTrackData(track, trackNew) - if 'flacCorrupted' in track: del track['flacCorrupted'] - return downloadTrackObj(dz, trackAPI, settings, bitrate, queueItem, extraTrack=track, interface=interface) - elif not 'searched' in track and settings['fallbackSearch']: - logger.warn(f"[{track['mainArtist']['name']} - {track['title']}] Track not found at desired bitrate, searching for alternative") - searchedId = dz.get_track_from_metadata(track['mainArtist']['name'], track['title'], - track['album']['title']) - if searchedId != 0: - trackNew = dz.get_track_gw(searchedId) - track = parseEssentialTrackData(track, trackNew) - if 'flacCorrupted' in track: del track['flacCorrupted'] - track['searched'] = True - return downloadTrackObj(dz, trackAPI, settings, bitrate, queueItem, extraTrack=track, - interface=interface) - else: - logger.error(f"[{track['mainArtist']['name']} - {track['title']}] Track not found at desired bitrate and no alternative found!") - trackCompletePercentage(trackAPI, queueItem, interface) - result['error'] = { - 'message': "Track not found at desired bitrate and no alternative found!", - 'errid': 'wrongBitrateNoAlternative', - 'data': { - 'id': track['id'], - 'title': track['title'], - 'artist': track['mainArtist']['name'] - } - } - queueItem.failed += 1 - queueItem.errors.append(result['error']) - if interface: - interface.send("updateQueue", {'uuid': queueItem.uuid, 'failed': True, 'data': result['error']['data'], - 'error': result['error']['message'], 'errid': result['error']['errid']}) - return result - else: - logger.error(f"[{track['mainArtist']['name']} - {track['title']}] Track not found at desired bitrate. Enable fallback to lower bitrates to fix this issue.") - trackCompletePercentage(trackAPI, queueItem, interface) - result['error'] = { - 'message': "Track not found at desired bitrate.", - 'errid': 'wrongBitrate', - 'data': { - 'id': track['id'], - 'title': track['title'], - 'artist': track['mainArtist']['name'] - } - } - queueItem.failed += 1 - queueItem.errors.append(result['error']) - if interface: - interface.send("updateQueue", {'uuid': queueItem.uuid, 'failed': True, 'data': result['error']['data'], - 'error': result['error']['message'], 'errid': result['error']['errid']}) - return result - elif selectedBitrate == -200: - logger.error(f"[{track['mainArtist']['name']} - {track['title']}] This track is not available in 360 Reality Audio format. Please select another format.") - trackCompletePercentage(trackAPI, queueItem, interface) - result['error'] = { - 'message': "Track is not available in Reality Audio 360.", - 'errid': 'no360RA', - 'data': { - 'id': track['id'], - 'title': track['title'], - 'artist': track['mainArtist']['name'] - } - } - queueItem.failed += 1 - queueItem.errors.append(result['error']) - if interface: - interface.send("updateQueue", {'uuid': queueItem.uuid, 'failed': True, 'data': result['error']['data'], - 'error': result['error']['message'], 'errid': result['error']['errid']}) - return result - track['selectedFormat'] = selectedBitrate - if settings['tags']['savePlaylistAsCompilation'] and "playlist" in track: - track['trackNumber'] = trackAPI["POSITION"] - track['discNumber'] = "1" - track['album'] = {**track['album'], **track['playlist']} - else: - if 'date' in track['album']: - track['date'] = track['album']['date'] - track['album']['picUrl'] = "https://e-cdns-images.dzcdn.net/images/cover/{}/{}x{}-{}".format( - track['album']['pic'], settings['embeddedArtworkSize'], settings['embeddedArtworkSize'], - f'000000-{settings["jpegImageQuality"]}-0-0.jpg') - track['album']['bitrate'] = selectedBitrate - track['dateString'] = formatDate(track['date'], settings['dateFormat']) - track['album']['dateString'] = formatDate(track['album']['date'], settings['dateFormat']) - - # Check if user wants the feat in the title - # 0 => do not change - # 1 => remove from title - # 2 => add to title - # 3 => remove from title and album title - if settings['featuredToTitle'] == "1": - track['title'] = track['title_clean'] - elif settings['featuredToTitle'] == "2": - track['title'] = track['title_feat'] - elif settings['featuredToTitle'] == "3": - track['title'] = track['title_clean'] - track['album']['title'] = track['album']['title_clean'] - - # Remove (Album Version) from tracks that have that - if settings['removeAlbumVersion']: - if "Album Version" in track['title']: - track['title'] = re.sub(r' ?\(Album Version\)', "", track['title']).strip() - - # Generate artist tag if needed - if settings['tags']['multiArtistSeparator'] != "default": - if settings['tags']['multiArtistSeparator'] == "andFeat": - track['artistsString'] = track['mainArtistsString'] - if 'featArtistsString' in track and settings['featuredToTitle'] != "2": - track['artistsString'] += " " + track['featArtistsString'] - else: - track['artistsString'] = settings['tags']['multiArtistSeparator'].join(track['artists']) - else: - track['artistsString'] = ", ".join(track['artists']) - - # Change Title and Artists casing if needed - if settings['titleCasing'] != "nothing": - track['title'] = changeCase(track['title'], settings['titleCasing']) - if settings['artistCasing'] != "nothing": - track['artistsString'] = changeCase(track['artistsString'], settings['artistCasing']) - for i, artist in enumerate(track['artists']): - track['artists'][i] = changeCase(artist, settings['artistCasing']) - - # Generate filename and filepath from metadata - filename = generateFilename(track, trackAPI, settings) - (filepath, artistPath, coverPath, extrasPath) = generateFilepath(track, trackAPI, settings) - - if queueItem.cancel: - result['cancel'] = True - return result - # Download and cache coverart - if settings['tags']['savePlaylistAsCompilation'] and "_EXTRA_PLAYLIST" in trackAPI: - track['album']['picPath'] = os.path.join(TEMPDIR, - f"pl{trackAPI['_EXTRA_PLAYLIST']['id']}_{settings['embeddedArtworkSize']}.jpg") - else: - track['album']['picPath'] = os.path.join(TEMPDIR, - f"alb{track['album']['id']}_{settings['embeddedArtworkSize']}.jpg") - logger.info(f"[{track['mainArtist']['name']} - {track['title']}] Getting the album cover") - track['album']['picPath'] = downloadImage(track['album']['picUrl'], track['album']['picPath']) - - if os.path.sep in filename: - tempPath = filename[:filename.rfind(os.path.sep)] - filepath = os.path.join(filepath, tempPath) - filename = filename[filename.rfind(os.path.sep) + len(os.path.sep):] - makedirs(filepath, exist_ok=True) - writepath = os.path.join(filepath, filename + extensions[track['selectedFormat']]) - - # Save lyrics in lrc file - if settings['syncedLyrics'] and 'sync' in track['lyrics']: - if not os.path.isfile(os.path.join(filepath, filename + '.lrc')) or settings['overwriteFile'] in ['y', 't']: - with open(os.path.join(filepath, filename + '.lrc'), 'wb') as f: - f.write(track['lyrics']['sync'].encode('utf-8')) - - # Save local album art - if coverPath: - result['albumURLs'] = [] - for format in settings['localArtworkFormat'].split(","): - if format in ["png","jpg"]: - url = track['album']['picUrl'].replace( - f"{settings['embeddedArtworkSize']}x{settings['embeddedArtworkSize']}", - f"{settings['localArtworkSize']}x{settings['localArtworkSize']}") - if format == "png": - url = url[:url.find("000000-")]+"none-100-0-0.png" - result['albumURLs'].append({'url': url, 'ext': format}) - result['albumPath'] = os.path.join(coverPath, - f"{settingsRegexAlbum(settings['coverImageTemplate'], track['album'], settings, trackAPI['_EXTRA_PLAYLIST'] if'_EXTRA_PLAYLIST' in trackAPI else None)}") - - # Save artist art - if artistPath: - result['artistURLs'] = [] - for format in settings['localArtworkFormat'].split(","): - if format in ["png","jpg"]: - url = "" - if track['album']['mainArtist']['pic'] != "": - url = "https://e-cdns-images.dzcdn.net/images/artist/{}/{}x{}-{}".format( - track['album']['mainArtist']['pic'], settings['localArtworkSize'], settings['localArtworkSize'], - 'none-100-0-0.png' if format == "png" else f'000000-{settings["jpegImageQuality"]}-0-0.jpg') - elif format == "jpg": - url = "https://e-cdns-images.dzcdn.net/images/artist//{}x{}-{}".format( - settings['localArtworkSize'], settings['localArtworkSize'], f'000000-{settings["jpegImageQuality"]}-0-0.jpg') - if url: - result['artistURLs'].append({'url': url, 'ext': format}) - result['artistPath'] = os.path.join(artistPath, - f"{settingsRegexArtist(settings['artistImageTemplate'], track['album']['mainArtist'], settings)}") - - trackAlreadyDownloaded = os.path.isfile(writepath) - if trackAlreadyDownloaded and settings['overwriteFile'] == 'b': - baseFilename = os.path.join(filepath, filename) - i = 1 - currentFilename = baseFilename+' ('+str(i)+')'+ extensions[track['selectedFormat']] - while os.path.isfile(currentFilename): - i += 1 - currentFilename = baseFilename+' ('+str(i)+')'+ extensions[track['selectedFormat']] - trackAlreadyDownloaded = False - writepath = currentFilename - # Data for m3u file - if extrasPath: - result['extrasPath'] = extrasPath - result['playlistPosition'] = writepath[len(extrasPath):] - if "playlist" in track: - result['playlistURLs'] = [] - if 'dzcdn.net' in track['playlist']['picUrl']: - for format in settings['localArtworkFormat'].split(","): - if format in ["png","jpg"]: - url = track['playlist']['picUrl'].replace( - f"{settings['embeddedArtworkSize']}x{settings['embeddedArtworkSize']}", - f"{settings['localArtworkSize']}x{settings['localArtworkSize']}") - if format == "png": - url = url[:url.find("000000-")]+"none-100-0-0.png" - result['playlistURLs'].append({'url': url, 'ext': format}) - else: - result['playlistURLs'].append({'url': track['playlist']['picUrl'], 'ext': 'jpg'}) - track['playlist']['id'] = "pl_" + str(trackAPI['_EXTRA_PLAYLIST']['id']) - track['playlist']['genre'] = ["Compilation", ] - track['playlist']['bitrate'] = selectedBitrate - track['playlist']['dateString'] = formatDate(track['playlist']['date'], settings['dateFormat']) - result['playlistCover'] = f"{settingsRegexAlbum(settings['coverImageTemplate'], track['playlist'], settings, trackAPI['_EXTRA_PLAYLIST'])}" - - track['downloadUrl'] = dz.get_track_stream_url(track['id'], track['MD5'], track['mediaVersion'], - track['selectedFormat']) - if not trackAlreadyDownloaded or settings['overwriteFile'] == 'y': - logger.info(f"[{track['mainArtist']['name']} - {track['title']}] Downloading the track") - def downloadMusic(dz, track, trackAPI, queueItem, interface, writepath, result, settings): - try: - with open(writepath, 'wb') as stream: - stream_track(dz, track, stream, trackAPI, queueItem, interface) - except downloadCancelled: - remove(writepath) - result['cancel'] = True - return 1 - except HTTPError: - remove(writepath) - if track['fallbackId'] != 0: - logger.warn(f"[{track['mainArtist']['name']} - {track['title']}] Track not available, using fallback id") - trackNew = dz.get_track_gw(track['fallbackId']) - track = parseEssentialTrackData(track, trackNew) - if 'flacCorrupted' in track: del track['flacCorrupted'] - return 2 - elif not 'searched' in track and settings['fallbackSearch']: - logger.warn(f"[{track['mainArtist']['name']} - {track['title']}] Track not available, searching for alternative") - searchedId = dz.get_track_from_metadata(track['mainArtist']['name'], track['title'], - track['album']['title']) - if searchedId != 0: - trackNew = dz.get_track_gw(searchedId) - track = parseEssentialTrackData(track, trackNew) - if 'flacCorrupted' in track: del track['flacCorrupted'] - track['searched'] = True - return 2 - else: - logger.error(f"[{track['mainArtist']['name']} - {track['title']}] Track not available on deezer's servers and no alternative found!") - trackCompletePercentage(trackAPI, queueItem, interface) - result['error'] = { - 'message': "Track not available on deezer's servers and no alternative found!", - 'errid': 'notAvailableNoAlternative', - 'data': { - 'id': track['id'], - 'title': track['title'], - 'artist': track['mainArtist']['name'] - } - } - queueItem.failed += 1 - queueItem.errors.append(result['error']) - if interface: - interface.send("updateQueue", {'uuid': queueItem.uuid, 'failed': True, 'data': result['error']['data'], - 'error': result['error']['message'], 'errid': result['error']['errid']}) - return 1 - else: - logger.error(f"[{track['mainArtist']['name']} - {track['title']}] Track not available on deezer's servers!") - trackCompletePercentage(trackAPI, queueItem, interface) - result['error'] = { - 'message': "Track not available on deezer's servers!", - 'errid': 'notAvailable', - 'data': { - 'id': track['id'], - 'title': track['title'], - 'artist': track['mainArtist']['name'] - } - } - queueItem.failed += 1 - queueItem.errors.append(result['error']) - if interface: - interface.send("updateQueue", {'uuid': queueItem.uuid, 'failed': True, 'data': result['error']['data'], - 'error': result['error']['message'], 'errid': result['error']['errid']}) - return 1 - except Exception as e: - logger.exception(str(e)) - logger.warn(f"[{track['mainArtist']['name']} - {track['title']}] Error while downloading the track, trying again in 5s...") - sleep(5) - return downloadMusic(dz, track, trackAPI, queueItem, interface, writepath, result, settings) - return 0 - outcome = downloadMusic(dz, track, trackAPI, queueItem, interface, writepath, result, settings) - if outcome == 1: - return result - elif outcome == 2: - return downloadTrackObj(dz, trackAPI, settings, bitrate, queueItem, extraTrack=track, interface=interface) - else: - logger.info(f"[{track['mainArtist']['name']} - {track['title']}] Skipping track as it's already downloaded") - trackCompletePercentage(trackAPI, queueItem, interface) - if (not trackAlreadyDownloaded or settings['overwriteFile'] in ['t', 'y']) and not 'localTrack' in track: - logger.info(f"[{track['mainArtist']['name']} - {track['title']}] Applying tags to the track") - if track['selectedFormat'] in [3, 1, 8]: - tagID3(writepath, track, settings['tags']) - elif track['selectedFormat'] == 9: - try: - tagFLAC(writepath, track, settings['tags']) - except FLACNoHeaderError: - remove(writepath) - logger.warn(f"[{track['mainArtist']['name']} - {track['title']}] Track not available in FLAC, falling back if necessary") - trackRemovePercentage(trackAPI, queueItem, interface) - track['flacCorrupted'] = True - return downloadTrackObj(dz, trackAPI, settings, bitrate, queueItem, extraTrack=track, interface=interface) - if 'searched' in track: - result['searched'] = f'{track["mainArtist"]["name"]} - {track["title"]}' - logger.info(f"[{track['mainArtist']['name']} - {track['title']}] Track download completed") - queueItem.downloaded += 1 - if interface: - interface.send("updateQueue", {'uuid': queueItem.uuid, 'downloaded': True, 'downloadPath': writepath}) - return result - - -def downloadTrackObj_wrap(dz, track, settings, bitrate, queueItem, interface): - try: - result = downloadTrackObj(dz, track, settings, bitrate, queueItem, interface=interface) - except Exception as e: - logger.exception(str(e)) - result = {'error': { - 'message': str(e), - 'data': { - 'id': track['SNG_ID'], - 'title': track['SNG_TITLE'] + (track['VERSION'] if 'VERSION' in track and track['VERSION'] and not track['VERSION'] in track['SNG_TITLE'] else ""), - 'artist': track['ART_NAME'] - } - } - } - queueItem.failed += 1 - queueItem.errors.append(result['error']) - if interface: - interface.send("updateQueue", {'uuid': queueItem.uuid, 'failed': True, 'data': result['error']['data'], - 'error': result['error']['message']}) - return result - - -def download(dz, sp, queueItem, interface=None): - global downloadPercentage, lastPercentage - settings = queueItem.settings - bitrate = queueItem.bitrate - downloadPercentage = 0 - lastPercentage = 0 - if isinstance(queueItem, QIConvertable): - sp.convert_spotify_playlist(dz, queueItem, settings, interface=interface) - if isinstance(queueItem, QISingle): - try: - result = downloadTrackObj(dz, queueItem.single, settings, bitrate, queueItem, interface=interface) - except Exception as e: - logger.exception(str(e)) - result = {'error': { - 'message': str(e), - 'data': { - 'id': queueItem.single['SNG_ID'], - 'title': queueItem.single['SNG_TITLE'] + (queueItem.single['VERSION'] if 'VERSION' in queueItem.single and queueItem.single['VERSION'] and not queueItem.single['VERSION'] in queueItem.single['SNG_TITLE'] else ""), - 'mainArtist': {'name': queueItem.single['ART_NAME']} - } - } - } - queueItem.failed += 1 - queueItem.errors.append(result['error']) - if interface: - interface.send("updateQueue", {'uuid': queueItem.uuid, 'failed': True, 'data': result['error']['data'], - 'error': result['error']['message']}) - download_path = after_download_single(result, settings) - elif isinstance(queueItem, QICollection): - playlist = [None] * len(queueItem.collection) - with ThreadPoolExecutor(settings['queueConcurrency']) as executor: - for pos, track in enumerate(queueItem.collection, start=0): - playlist[pos] = executor.submit(downloadTrackObj_wrap, dz, track, settings, bitrate, queueItem, - interface=interface) - download_path = after_download(playlist, settings, queueItem) - if interface: - if queueItem.cancel: - interface.send('currentItemCancelled', queueItem.uuid) - interface.send("removedFromQueue", queueItem.uuid) - else: - interface.send("finishDownload", queueItem.uuid) - return download_path - - -def after_download(tracks, settings, queueItem): - extrasPath = None - playlist = [None] * len(tracks) - playlistCover = None - playlistURLs = [] - errors = "" - searched = "" - for index in range(len(tracks)): - result = tracks[index].result() - if 'cancel' in result: - return None - if 'error' in result: - if not 'data' in result['error']: - result['error']['data'] = {'id': 0, 'title': 'Unknown', 'artist': 'Unknown'} - errors += f"{result['error']['data']['id']} | {result['error']['data']['artist']} - {result['error']['data']['title']} | {result['error']['message']}\r\n" - if 'searched' in result: - searched += result['searched'] + "\r\n" - if not extrasPath and 'extrasPath' in result: - extrasPath = result['extrasPath'] - if not playlistCover and 'playlistCover' in result: - playlistCover = result['playlistCover'] - playlistURLs = result['playlistURLs'] - if settings['saveArtwork'] and 'albumPath' in result: - for image in result['albumURLs']: - downloadImage(image['url'], f"{result['albumPath']}.{image['ext']}", settings['overwriteFile']) - if settings['saveArtworkArtist'] and 'artistPath' in result: - for image in result['artistURLs']: - downloadImage(image['url'], f"{result['artistPath']}.{image['ext']}", settings['overwriteFile']) - if 'playlistPosition' in result: - playlist[index] = result['playlistPosition'] - else: - playlist[index] = "" - if not extrasPath: - extrasPath = settings['downloadLocation'] - if settings['logErrors'] and errors != "": - with open(os.path.join(extrasPath, 'errors.txt'), 'wb') as f: - f.write(errors.encode('utf-8')) - if settings['saveArtwork'] and playlistCover and not settings['tags']['savePlaylistAsCompilation']: - for image in playlistURLs: - downloadImage(image['url'], os.path.join(extrasPath, playlistCover)+f".{image['ext']}", settings['overwriteFile']) - if settings['logSearched'] and searched != "": - with open(os.path.join(extrasPath, 'searched.txt'), 'wb') as f: - f.write(searched.encode('utf-8')) - if settings['createM3U8File']: - filename = settingsRegexPlaylistFile(settings['playlistFilenameTemplate'], queueItem, settings) or "playlist" - with open(os.path.join(extrasPath, filename+'.m3u8'), 'wb') as f: - for line in playlist: - f.write((line + "\n").encode('utf-8')) - if settings['executeCommand'] != "": - execute(settings['executeCommand'].replace("%folder%", extrasPath)) - return extrasPath - - -def after_download_single(track, settings): - if 'cancel' in track: - return None - if 'extrasPath' not in track: - track['extrasPath'] = settings['downloadLocation'] - if settings['saveArtwork'] and 'albumPath' in track: - for image in track['albumURLs']: - downloadImage(image['url'], f"{track['albumPath']}.{image['ext']}", settings['overwriteFile']) - if settings['saveArtworkArtist'] and 'artistPath' in track: - for image in track['artistURLs']: - downloadImage(image['url'], f"{track['artistPath']}.{image['ext']}", settings['overwriteFile']) - if settings['logSearched'] and 'searched' in track: - with open(os.path.join(track['extrasPath'], 'searched.txt'), 'wb+') as f: - orig = f.read().decode('utf-8') - if not track['searched'] in orig: - if orig != "": - orig += "\r\n" - orig += track['searched'] + "\r\n" - f.write(orig.encode('utf-8')) - if settings['executeCommand'] != "": - execute(settings['executeCommand'].replace("%folder%", track['extrasPath']).replace("%filename%", track['playlistPosition'])) - return track['extrasPath'] - - -class downloadCancelled(Exception): - """Base class for exceptions in this module.""" - pass diff --git a/deemix/utils/misc.py b/deemix/utils/misc.py index aa42c0e..e30520f 100644 --- a/deemix/utils/misc.py +++ b/deemix/utils/misc.py @@ -34,6 +34,31 @@ def changeCase(str, type): return str +def removeFeatures(title): + clean = title + if "(feat." in clean.lower(): + pos = clean.lower().find("(feat.") + tempTrack = clean[:pos] + if ")" in clean: + tempTrack += clean[clean.find(")", pos + 1) + 1:] + clean = tempTrack.strip() + return clean + + +def andCommaConcat(lst): + tot = len(lst) + result = "" + for i, art in enumerate(lst): + result += art + track['commaArtistsString'] += art + if tot != i + 1: + if tot - 1 == i + 1: + result += " & " + else: + result += ", " + return result + + def getIDFromLink(link, type): if '?' in link: link = link[:link.find('?')] diff --git a/deemix/utils/pathtemplates.py b/deemix/utils/pathtemplates.py index cffcb83..96259cd 100644 --- a/deemix/utils/pathtemplates.py +++ b/deemix/utils/pathtemplates.py @@ -94,10 +94,10 @@ def generateFilepath(track, trackAPI, settings): 'savePlaylistAsCompilation']) or (settings['createArtistFolder'] and '_EXTRA_PLAYLIST' in trackAPI and settings['createStructurePlaylist']) ): - if (int(track['id']) < 0 and not 'mainArtist' in track['album']): - track['album']['mainArtist'] = track['mainArtist'] + if (int(track.id) < 0 and not 'mainArtist' in track.album): + track.album['mainArtist'] = track.mainArtist filepath += antiDot( - settingsRegexArtist(settings['artistNameTemplate'], track['album']['mainArtist'], settings)) + pathSep + settingsRegexArtist(settings['artistNameTemplate'], track.album['mainArtist'], settings)) + pathSep artistPath = filepath if (settings['createAlbumFolder'] and @@ -107,7 +107,7 @@ def generateFilepath(track, trackAPI, settings): '_EXTRA_PLAYLIST' in trackAPI and settings['createStructurePlaylist'])) ): filepath += antiDot( - settingsRegexAlbum(settings['albumNameTemplate'], track['album'], settings, + settingsRegexAlbum(settings['albumNameTemplate'], track.album, settings, trackAPI['_EXTRA_PLAYLIST'] if'_EXTRA_PLAYLIST' in trackAPI else None)) + pathSep coverPath = filepath @@ -115,55 +115,55 @@ def generateFilepath(track, trackAPI, settings): extrasPath = filepath if ( - int(track['album']['discTotal']) > 1 and ( + int(track.album['discTotal']) > 1 and ( (settings['createAlbumFolder'] and settings['createCDFolder']) and (not 'SINGLE_TRACK' in trackAPI or ('SINGLE_TRACK' in trackAPI and settings['createSingleFolder'])) and (not '_EXTRA_PLAYLIST' in trackAPI or ( '_EXTRA_PLAYLIST' in trackAPI and settings['tags']['savePlaylistAsCompilation']) or ( '_EXTRA_PLAYLIST' in trackAPI and settings['createStructurePlaylist'])) )): - filepath += 'CD' + str(track['discNumber']) + pathSep + filepath += 'CD' + str(track.discNumber) + pathSep return (filepath, artistPath, coverPath, extrasPath) def settingsRegex(filename, track, settings, playlist=None): - filename = filename.replace("%title%", fixName(track['title'], settings['illegalCharacterReplacer'])) - filename = filename.replace("%artist%", fixName(track['mainArtist']['name'], settings['illegalCharacterReplacer'])) - filename = filename.replace("%artists%", fixName(track['commaArtistsString'], settings['illegalCharacterReplacer'])) - filename = filename.replace("%allartists%", fixName(track['artistsString'], settings['illegalCharacterReplacer'])) - filename = filename.replace("%mainartists%", fixName(track['mainArtistsString'], settings['illegalCharacterReplacer'])) - filename = filename.replace("%featartists%", fixName('('+track['featArtistsString']+')', settings['illegalCharacterReplacer']) if 'featArtistsString' in track else "") - filename = filename.replace("%album%", fixName(track['album']['title'], settings['illegalCharacterReplacer'])) + filename = filename.replace("%title%", fixName(track.title, settings['illegalCharacterReplacer'])) + filename = filename.replace("%artist%", fixName(track.mainArtist['name'], settings['illegalCharacterReplacer'])) + filename = filename.replace("%artists%", fixName(track.commaArtistsString, settings['illegalCharacterReplacer'])) + filename = filename.replace("%allartists%", fixName(track.artistsString, settings['illegalCharacterReplacer'])) + filename = filename.replace("%mainartists%", fixName(track.mainArtistsString, settings['illegalCharacterReplacer'])) + filename = filename.replace("%featartists%", fixName('('+track.featArtistsString+')', settings['illegalCharacterReplacer']) if 'featArtistsString' in track else "") + filename = filename.replace("%album%", fixName(track.album['title'], settings['illegalCharacterReplacer'])) filename = filename.replace("%albumartist%", - fixName(track['album']['mainArtist']['name'], settings['illegalCharacterReplacer'])) - filename = filename.replace("%tracknumber%", pad(track['trackNumber'], track['album']['trackTotal'] if int( + fixName(track.album['mainArtist']['name'], settings['illegalCharacterReplacer'])) + filename = filename.replace("%tracknumber%", pad(track.trackNumber, track.album['trackTotal'] if int( settings['paddingSize']) == 0 else 10 ** (int(settings['paddingSize']) - 1), settings['padTracks'])) - filename = filename.replace("%tracktotal%", str(track['album']['trackTotal'])) - filename = filename.replace("%discnumber%", str(track['discNumber'])) - filename = filename.replace("%disctotal%", str(track['album']['discTotal'])) - if len(track['album']['genre']) > 0: + filename = filename.replace("%tracktotal%", str(track.album['trackTotal'])) + filename = filename.replace("%discnumber%", str(track.discNumber)) + filename = filename.replace("%disctotal%", str(track.album['discTotal'])) + if len(track.album['genre']) > 0: filename = filename.replace("%genre%", - fixName(track['album']['genre'][0], settings['illegalCharacterReplacer'])) + fixName(track.album['genre'][0], settings['illegalCharacterReplacer'])) else: filename = filename.replace("%genre%", "Unknown") - filename = filename.replace("%year%", str(track['date']['year'])) - filename = filename.replace("%date%", track['dateString']) - filename = filename.replace("%bpm%", str(track['bpm'])) - filename = filename.replace("%label%", fixName(track['album']['label'], settings['illegalCharacterReplacer'])) - filename = filename.replace("%isrc%", track['ISRC']) - filename = filename.replace("%upc%", track['album']['barcode']) - filename = filename.replace("%explicit%", "(Explicit)" if track['explicit'] else "") + filename = filename.replace("%year%", str(track.date['year'])) + filename = filename.replace("%date%", track.dateString) + filename = filename.replace("%bpm%", str(track.bpm)) + filename = filename.replace("%label%", fixName(track.album['label'], settings['illegalCharacterReplacer'])) + filename = filename.replace("%isrc%", track.ISRC) + filename = filename.replace("%upc%", track.album['barcode']) + filename = filename.replace("%explicit%", "(Explicit)" if track.explicit else "") - filename = filename.replace("%track_id%", str(track['id'])) - filename = filename.replace("%album_id%", str(track['album']['id'])) - filename = filename.replace("%artist_id%", str(track['mainArtist']['id'])) + filename = filename.replace("%track_id%", str(track.id)) + filename = filename.replace("%album_id%", str(track.album['id'])) + filename = filename.replace("%artist_id%", str(track.mainArtist['id'])) if playlist: filename = filename.replace("%playlist_id%", str(playlist['id'])) - filename = filename.replace("%position%", pad(track['position'], playlist['nb_tracks'] if int( + filename = filename.replace("%position%", pad(track.position, playlist['nb_tracks'] if int( settings['paddingSize']) == 0 else 10 ** (int(settings['paddingSize']) - 1), settings['padTracks'])) else: - filename = filename.replace("%position%", pad(track['trackNumber'], track['album']['trackTotal'] if int( + filename = filename.replace("%position%", pad(track.trackNumber, track.album['trackTotal'] if int( settings['paddingSize']) == 0 else 10 ** (int(settings['paddingSize']) - 1), settings['padTracks'])) filename = filename.replace('\\', pathSep).replace('/', pathSep) return antiDot(fixLongName(filename)) diff --git a/deemix/utils/taggers.py b/deemix/utils/taggers.py index 7f54730..26f63df 100644 --- a/deemix/utils/taggers.py +++ b/deemix/utils/taggers.py @@ -13,75 +13,75 @@ def tagID3(stream, track, save): tag = ID3() if save['title']: - tag.add(TIT2(text=track['title'])) - - if save['artist'] and len(track['artists']): + tag.add(TIT2(text=track.title)) + + if save['artist'] and len(track.artists): if save['multiArtistSeparator'] != "default": if save['multiArtistSeparator'] == "nothing": - tag.add(TPE1(text=track['mainArtist']['name'])) + tag.add(TPE1(text=track.mainArtist['name'])) else: - tag.add(TPE1(text=track['artistsString'])) - tag.add(TXXX(desc="ARTISTS", text=track['artists'])) + tag.add(TPE1(text=track.artistsString)) + tag.add(TXXX(desc="ARTISTS", text=track.artists)) else: - tag.add(TPE1(text=track['artists'])) + tag.add(TPE1(text=track.artists)) if save['album']: - tag.add(TALB(text=track['album']['title'])) + tag.add(TALB(text=track.album['title'])) - if save['albumArtist'] and len(track['album']['artists']): - if save['singleAlbumArtist'] and track['album']['mainArtist']['save']: - tag.add(TPE2(text=track['album']['mainArtist']['name'])) + if save['albumArtist'] and len(track.album['artists']): + if save['singleAlbumArtist'] and track.album['mainArtist']['save']: + tag.add(TPE2(text=track.album['mainArtist']['name'])) else: - tag.add(TPE2(text=track['album']['artists'])) + tag.add(TPE2(text=track.album['artists'])) if save['trackNumber']: tag.add(TRCK( - text=str(track['trackNumber']) + ("/" + str(track['album']['trackTotal']) if save['trackTotal'] else ""))) + text=str(track.trackNumber) + ("/" + str(track.album['trackTotal']) if save['trackTotal'] else ""))) if save['discNumber']: tag.add( - TPOS(text=str(track['discNumber']) + ("/" + str(track['album']['discTotal']) if save['discTotal'] else ""))) + TPOS(text=str(track.discNumber) + ("/" + str(track.album['discTotal']) if save['discTotal'] else ""))) if save['genre']: - tag.add(TCON(text=track['album']['genre'])) + tag.add(TCON(text=track.album['genre'])) if save['year']: - tag.add(TYER(text=str(track['date']['year']))) + tag.add(TYER(text=str(track.date['year']))) if save['date']: - tag.add(TDAT(text=str(track['date']['month']) + str(track['date']['day']))) + tag.add(TDAT(text=str(track.date['month']) + str(track.date['day']))) if save['length']: - tag.add(TLEN(text=str(int(track['duration'])*1000))) + tag.add(TLEN(text=str(int(track.duration)*1000))) if save['bpm']: - tag.add(TBPM(text=str(track['bpm']))) + tag.add(TBPM(text=str(track.bpm))) if save['label']: - tag.add(TPUB(text=track['album']['label'])) + tag.add(TPUB(text=track.album['label'])) if save['isrc']: - tag.add(TSRC(text=track['ISRC'])) + tag.add(TSRC(text=track.ISRC)) if save['barcode']: - tag.add(TXXX(desc="BARCODE", text=track['album']['barcode'])) + tag.add(TXXX(desc="BARCODE", text=track.album['barcode'])) if save['explicit']: - tag.add(TXXX(desc="ITUNESADVISORY", text="1" if track['explicit'] else "0")) + tag.add(TXXX(desc="ITUNESADVISORY", text="1" if track.explicit else "0")) if save['replayGain']: - tag.add(TXXX(desc="REPLAYGAIN_TRACK_GAIN", text=track['replayGain'])) - if 'unsync' in track['lyrics'] and save['lyrics']: - tag.add(USLT(text=track['lyrics']['unsync'])) + tag.add(TXXX(desc="REPLAYGAIN_TRACK_GAIN", text=track.replayGain)) + if 'unsync' in track.lyrics and save['lyrics']: + tag.add(USLT(text=track.lyrics['unsync'])) involved_people = [] - for role in track['contributors']: + for role in track.contributors: if role in ['author', 'engineer', 'mixer', 'producer', 'writer']: - for person in track['contributors'][role]: + for person in track.contributors[role]: involved_people.append([role, person]) elif role == 'composer' and save['composer']: - tag.add(TCOM(text=track['contributors']['composer'])) + tag.add(TCOM(text=track.contributors['composer'])) if len(involved_people) > 0 and save['involvedPeople']: tag.add(IPLS(people=involved_people)) if save['copyright']: - tag.add(TCOP(text=track['copyright'])) + tag.add(TCOP(text=track.copyright)) if save['savePlaylistAsCompilation'] and "playlist" in track: tag.add(TCMP(text="1")) - if save['cover'] and track['album']['picPath']: - with open(track['album']['picPath'], 'rb') as f: + if save['cover'] and track.album['picPath']: + with open(track.album['picPath'], 'rb') as f: tag.add( - APIC(3, 'image/jpeg' if track['album']['picPath'].endswith('jpg') else 'image/png', 3, desc='cover', data=f.read())) + APIC(3, 'image/jpeg' if track.album['picPath'].endswith('jpg') else 'image/png', 3, desc='cover', data=f.read())) tag.save(stream, v1=2 if save['saveID3v1'] else 0, v2_version=3, v23_sep=None if save['useNullSeparator'] else '/') @@ -94,75 +94,75 @@ def tagFLAC(stream, track, save): tag.clear_pictures() if save['title']: - tag["TITLE"] = track['title'] + tag["TITLE"] = track.title - if save['artist'] and len(track['artists']): + if save['artist'] and len(track.artists): if save['multiArtistSeparator'] != "default": if save['multiArtistSeparator'] == "nothing": - tag["ARTIST"] = track['mainArtist']['name'] + tag["ARTIST"] = track.mainArtist['name'] else: - tag["ARTIST"] = track['artistsString'] - tag["ARTISTS"] = track['artists'] + tag["ARTIST"] = track.artistsString + tag["ARTISTS"] = track.artists else: - tag["ARTIST"] = track['artists'] + tag["ARTIST"] = track.artists if save['album']: - tag["ALBUM"] = track['album']['title'] + tag["ALBUM"] = track.album['title'] - if save['albumArtist'] and len(track['album']['artists']): + if save['albumArtist'] and len(track.album['artists']): if save['singleAlbumArtist']: - tag["ALBUMARTIST"] = track['album']['mainArtist']['name'] + tag["ALBUMARTIST"] = track.album['mainArtist']['name'] else: - tag["ALBUMARTIST"] = track['album']['artists'] + tag["ALBUMARTIST"] = track.album['artists'] if save['trackNumber']: - tag["TRACKNUMBER"] = str(track['trackNumber']) + tag["TRACKNUMBER"] = str(track.trackNumber) if save['trackTotal']: - tag["TRACKTOTAL"] = str(track['album']['trackTotal']) + tag["TRACKTOTAL"] = str(track.album['trackTotal']) if save['discNumber']: - tag["DISCNUMBER"] = str(track['discNumber']) + tag["DISCNUMBER"] = str(track.discNumber) if save['discTotal']: - tag["DISCTOTAL"] = str(track['album']['discTotal']) + tag["DISCTOTAL"] = str(track.album['discTotal']) if save['genre']: - tag["GENRE"] = track['album']['genre'] + tag["GENRE"] = track.album['genre'] if save['date']: - tag["DATE"] = track['dateString'] + tag["DATE"] = track.dateString elif save['year']: - tag["YEAR"] = str(track['date']['year']) + tag["YEAR"] = str(track.date['year']) if save['length']: - tag["LENGTH"] = str(track['duration']) + tag["LENGTH"] = str(track.duration) if save['bpm']: - tag["BPM"] = str(track['bpm']) + tag["BPM"] = str(track.bpm) if save['label']: - tag["PUBLISHER"] = track['album']['label'] + tag["PUBLISHER"] = track.album['label'] if save['isrc']: - tag["ISRC"] = track['ISRC'] + tag["ISRC"] = track.ISRC if save['barcode']: - tag["BARCODE"] = track['album']['barcode'] + tag["BARCODE"] = track.album['barcode'] if save['explicit']: - tag["ITUNESADVISORY"] = "1" if track['explicit'] else "0" + tag["ITUNESADVISORY"] = "1" if track.explicit else "0" if save['replayGain']: - tag["REPLAYGAIN_TRACK_GAIN"] = track['replayGain'] - if 'unsync' in track['lyrics'] and save['lyrics']: - tag["LYRICS"] = track['lyrics']['unsync'] + tag["REPLAYGAIN_TRACK_GAIN"] = track.replayGain + if 'unsync' in track.lyrics and save['lyrics']: + tag["LYRICS"] = track.lyrics['unsync'] - for role in track['contributors']: + for role in track.contributors: if role in ['author', 'engineer', 'mixer', 'producer', 'writer', 'composer']: if save['involvedPeople'] and role != 'composer' or role == 'composer' and save['composer']: - tag[role] = track['contributors'][role] + tag[role] = track.contributors[role] elif role == 'musicpublisher' and save['involvedPeople']: - tag["ORGANIZATION"] = track['contributors']['musicpublisher'] + tag["ORGANIZATION"] = track.contributors['musicpublisher'] if save['copyright']: - tag["COPYRIGHT"] = track['copyright'] + tag["COPYRIGHT"] = track.copyright if save['savePlaylistAsCompilation'] and "playlist" in track: tag["COMPILATION"] = "1" - if save['cover'] and track['album']['picPath']: + if save['cover'] and track.album['picPath']: image = Picture() image.type = 3 - image.mime = 'image/jpeg' if track['album']['picPath'].endswith('jpg') else 'image/png' - with open(track['album']['picPath'], 'rb') as f: + image.mime = 'image/jpeg' if track.album['picPath'].endswith('jpg') else 'image/png' + with open(track.album['picPath'], 'rb') as f: image.data = f.read() tag.add_picture(image)