From 3f8d4d5ae9b945ad399f0015542c93402b090498 Mon Sep 17 00:00:00 2001 From: RemixDev Date: Fri, 14 Aug 2020 18:14:08 +0200 Subject: [PATCH 01/13] Moved local outside of settings --- deemix/__main__.py | 5 ++++- deemix/app/settings.py | 7 ++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/deemix/__main__.py b/deemix/__main__.py index ae5ce1b..029cecd 100644 --- a/deemix/__main__.py +++ b/deemix/__main__.py @@ -11,7 +11,10 @@ from os.path import isfile @click.option('-l', '--local', is_flag=True, help='Downloads in a local folder insted of using the default') @click.argument('url', nargs=-1, required=True) def download(bitrate, local, url): - settings = initSettings(local) + settings = initSettings() + if local: + settings['downloadLocation'] = randomString(12) + click.echo("Using a local download folder: "+settings['downloadLocation']) app.login() url = list(url) if isfile(url[0]): diff --git a/deemix/app/settings.py b/deemix/app/settings.py index eff73e5..e55e9b7 100644 --- a/deemix/app/settings.py +++ b/deemix/app/settings.py @@ -18,7 +18,7 @@ settings = {} defaultSettings = {} configDir = "" -def initSettings(localFolder = False, configFolder = None): +def initSettings(configFolder = None): global settings global defaultSettings global configDir @@ -37,10 +37,7 @@ def initSettings(localFolder = False, configFolder = None): settings = json.load(configFile) settingsCheck() - if localFolder: - settings['downloadLocation'] = randomString(12) - logger.info("Using a local download folder: "+settings['downloadLocation']) - elif settings['downloadLocation'] == "": + if settings['downloadLocation'] == "": settings['downloadLocation'] = path.join(localpaths.getHomeFolder(), 'deemix Music') saveSettings(settings) makedirs(settings['downloadLocation'], exist_ok=True) From a991ad04ec55d5c064474bbb0d9963fbd6937a8f Mon Sep 17 00:00:00 2001 From: RemixDev Date: Fri, 14 Aug 2020 19:50:37 +0200 Subject: [PATCH 02/13] Moved spotify conversion outside creating queue item --- deemix/app/downloader.py | 5 ++++- deemix/app/queuemanager.py | 22 ++++++++----------- deemix/app/spotify.py | 45 +++++++++++++++++++++++++++----------- 3 files changed, 45 insertions(+), 27 deletions(-) diff --git a/deemix/app/downloader.py b/deemix/app/downloader.py index 4f0e034..afd108e 100644 --- a/deemix/app/downloader.py +++ b/deemix/app/downloader.py @@ -947,12 +947,14 @@ def downloadTrackObj_wrap(dz, track, settings, bitrate, queueItem, interface): return result -def download(dz, queueItem, interface=None): +def download(dz, sp, queueItem, interface=None): global downloadPercentage, lastPercentage settings = queueItem['settings'] bitrate = queueItem['bitrate'] downloadPercentage = 0 lastPercentage = 0 + if '_EXTRA' in queueItem: + sp.convert_spotify_playlist(dz, queueItem, settings, interface=interface) if 'single' in queueItem: try: result = downloadTrackObj(dz, queueItem['single'], settings, bitrate, queueItem, interface=interface) @@ -988,6 +990,7 @@ def download(dz, queueItem, interface=None): interface.send("finishDownload", queueItem['uuid']) return { 'dz': dz, + 'sp': sp, 'interface': interface, 'download_path': download_path } diff --git a/deemix/app/queuemanager.py b/deemix/app/queuemanager.py index 9fde329..1bbda4c 100644 --- a/deemix/app/queuemanager.py +++ b/deemix/app/queuemanager.py @@ -53,10 +53,10 @@ def slimQueueItems(items): def slimQueueItem(item): light = item.copy() - if 'single' in light: - del light['single'] - if 'collection' in light: - del light['collection'] + propertiesToDelete = ['single', 'collection', 'unconverted', '_EXTRA'] + for property in propertiesToDelete: + if property in light: + del light[property] return light def generateQueueItem(dz, sp, url, settings, bitrate=None, albumAPI=None, interface=None): @@ -377,18 +377,14 @@ def generateQueueItem(dz, sp, url, settings, bitrate=None, albumAPI=None, interf result['error'] = "Spotify Features is not setted up correctly." result['errid'] = "spotifyDisabled" return result - if interface: - interface.send("startConvertingSpotifyPlaylist", str(id)) try: - playlist = sp.convert_spotify_playlist(dz, id, settings) + playlist = sp.adapt_spotify_playlist(dz, id, settings) except SpotifyException as e: result['error'] = "Wrong URL: "+e.msg[e.msg.find('\n')+2:] return result playlist['bitrate'] = bitrate playlist['uuid'] = f"{playlist['type']}_{id}_{bitrate}" result = playlist - if interface: - interface.send("finishConvertingSpotifyPlaylist", str(id)) else: logger.warn("URL not supported yet") result['error'] = "URL not supported yet" @@ -447,11 +443,11 @@ def addToQueue(dz, sp, url, settings, bitrate=None, interface=None): logger.info(f"[{queueItem['uuid']}] Added to queue.") queue.append(queueItem['uuid']) queueList[queueItem['uuid']] = queueItem - nextItem(dz, interface) + nextItem(dz, sp, interface) return True -def nextItem(dz, interface=None): +def nextItem(dz, sp, interface=None): global currentItem, queueList, queue if currentItem != "": return None @@ -463,7 +459,7 @@ def nextItem(dz, interface=None): if interface: interface.send("startDownload", currentItem) logger.info(f"[{currentItem}] Started downloading.") - result = download(dz, queueList[currentItem], interface) + result = download(dz, sp, queueList[currentItem], interface) callbackQueueDone(result) @@ -475,7 +471,7 @@ def callbackQueueDone(result): queueComplete.append(currentItem) logger.info(f"[{currentItem}] Finished downloading.") currentItem = "" - nextItem(result['dz'], result['interface']) + nextItem(result['dz'], result['sp'], result['interface']) def getQueue(): diff --git a/deemix/app/spotify.py b/deemix/app/spotify.py index 5542d74..5e2d2f4 100644 --- a/deemix/app/spotify.py +++ b/deemix/app/spotify.py @@ -174,7 +174,7 @@ class SpotifyHelper: json.dump(cache, spotifyCache) return dz_album - def convert_spotify_playlist(self, dz, playlist_id, settings): + def adapt_spotify_playlist(self, dz, playlist_id, settings): if not self.spotifyEnabled: raise spotifyFeaturesNotEnabled spotify_playlist = self.sp.playlist(playlist_id) @@ -199,21 +199,34 @@ class SpotifyHelper: playlistAPI['various_artist'] = dz.get_artist(5080) tracklistTmp = spotify_playlist['tracks']['items'] result['collection'] = [] - tracklist = [] + result['_EXTRA'] = {} + result['_EXTRA']['unconverted'] = [] while spotify_playlist['tracks']['next']: spotify_playlist['tracks'] = self.sp.next(spotify_playlist['tracks']) tracklistTmp += spotify_playlist['tracks']['items'] for item in tracklistTmp: if item['track']: - tracklist.append(item['track']) - totalSize = len(tracklist) + if item['track']['explicit']: + playlistAPI['explicit'] = True + result['_EXTRA']['unconverted'].append(item['track']) + totalSize = len(result['_EXTRA']['unconverted']) result['size'] = totalSize + if not 'explicit' in playlistAPI: + playlistAPI['explicit'] = False + result['_EXTRA']['playlistAPI'] = playlistAPI + return result + + def convert_spotify_playlist(self, dz, item, settings, interface=None): + convertPercentage = 0 + lastPercentage = 0 if path.isfile(path.join(self.configFolder, 'spotifyCache.json')): with open(path.join(self.configFolder, 'spotifyCache.json'), 'r') as spotifyCache: cache = json.load(spotifyCache) else: cache = {'tracks': {}, 'albums': {}} - for pos, track in enumerate(tracklist, start=1): + if interface: + interface.send("startConversion", item['uuid']) + for pos, track in enumerate(item['_EXTRA']['unconverted'], start=1): if str(track['id']) in cache['tracks']: trackID = cache['tracks'][str(track['id'])] else: @@ -234,18 +247,24 @@ class SpotifyHelper: } else: deezerTrack = dz.get_track_gw(trackID) - if 'EXPLICIT_LYRICS' in deezerTrack and deezerTrack['EXPLICIT_LYRICS'] == "1": - playlistAPI['explicit'] = True - deezerTrack['_EXTRA_PLAYLIST'] = playlistAPI + deezerTrack['_EXTRA_PLAYLIST'] = item['_EXTRA']['playlistAPI'] deezerTrack['POSITION'] = pos - deezerTrack['SIZE'] = totalSize + deezerTrack['SIZE'] = item['size'] deezerTrack['FILENAME_TEMPLATE'] = settings['playlistTracknameTemplate'] - result['collection'].append(deezerTrack) - if not 'explicit' in playlistAPI: - playlistAPI['explicit'] = False + item['collection'].append(deezerTrack) + + convertPercentage = (pos / item['size']) * 100 + print(convertPercentage) + if round(convertPercentage) != lastPercentage and round(convertPercentage) % 2 == 0: + lastPercentage = round(convertPercentage) + if interface: + interface.send("updateQueue", {'uuid': item['uuid'], 'conversion': lastPercentage}) + + del item['_EXTRA'] with open(path.join(self.configFolder, 'spotifyCache.json'), 'w') as spotifyCache: json.dump(cache, spotifyCache) - return result + if interface: + interface.send("startDownload", item['uuid']) def get_user_playlists(self, user): if not self.spotifyEnabled: From 8afdfc7042504731e76f1604187ada318c3ab8b8 Mon Sep 17 00:00:00 2001 From: RemixDev Date: Fri, 14 Aug 2020 20:31:37 +0200 Subject: [PATCH 03/13] Started reworking the library --- deemix/__main__.py | 11 ++- deemix/app/Settings.py | 168 +++++++++++++++++++++++++++++++++++++ deemix/app/__init__.py | 4 + deemix/app/default.json | 75 ----------------- deemix/app/settings.py | 103 ----------------------- deemix/utils/localpaths.py | 6 +- deemix/utils/misc.py | 9 -- deemix/utils/taggers.py | 20 ++++- 8 files changed, 199 insertions(+), 197 deletions(-) create mode 100644 deemix/app/Settings.py delete mode 100644 deemix/app/default.json delete mode 100644 deemix/app/settings.py diff --git a/deemix/__main__.py b/deemix/__main__.py index 029cecd..df63226 100644 --- a/deemix/__main__.py +++ b/deemix/__main__.py @@ -4,6 +4,12 @@ import click import deemix.app.cli as app from deemix.app.settings import initSettings from os.path import isfile +import random +import string + +def randomString(stringLength=8): + letters = string.ascii_lowercase + return ''.join(random.choice(letters) for i in range(stringLength)) @click.command() @@ -26,8 +32,5 @@ def download(bitrate, local, url): if local: click.echo(settings['downloadLocation']) #folder name output -def main(): - download() - if __name__ == '__main__': - main() + download() diff --git a/deemix/app/Settings.py b/deemix/app/Settings.py new file mode 100644 index 0000000..52628b9 --- /dev/null +++ b/deemix/app/Settings.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +import json +import os.path as path +from os import makedirs, listdir, remove +from deemix import __version__ as deemixVersion +import logging +import datetime +import platform + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger('deemix') + +import deemix.utils.localpaths as localpaths + +class Settings: + def __init__(self, configFolder=None): + self.settings = {} + self.configFolder = configFolder + if not self.configFolder: + self.configFolder = localpaths.getConfigFolder() + self.defaultSettings = { + "downloadLocation": path.join(localpaths.getHomeFolder(), 'deemix Music'), + "tracknameTemplate": "%artist% - %title%", + "albumTracknameTemplate": "%tracknumber% - %title%", + "playlistTracknameTemplate": "%position% - %artist% - %title%", + "createPlaylistFolder": True, + "playlistNameTemplate": "%playlist%", + "createArtistFolder": False, + "artistNameTemplate": "%artist%", + "createAlbumFolder": True, + "albumNameTemplate": "%artist% - %album%", + "createCDFolder": True, + "createStructurePlaylist": False, + "createSingleFolder": False, + "padTracks": True, + "paddingSize": "0", + "illegalCharacterReplacer": "_", + "queueConcurrency": 3, + "maxBitrate": "3", + "fallbackBitrate": True, + "fallbackSearch": False, + "logErrors": True, + "logSearched": False, + "saveDownloadQueue": False, + "overwriteFile": "n", + "createM3U8File": False, + "playlistFilenameTemplate": "playlist", + "syncedLyrics": False, + "embeddedArtworkSize": 800, + "localArtworkSize": 1400, + "localArtworkFormat": "jpg", + "saveArtwork": True, + "coverImageTemplate": "cover", + "saveArtworkArtist": False, + "artistImageTemplate": "folder", + "jpegImageQuality": 80, + "dateFormat": "Y-M-D", + "albumVariousArtists": True, + "removeAlbumVersion": False, + "removeDuplicateArtists": False, + "featuredToTitle": "0", + "titleCasing": "nothing", + "artistCasing": "nothing", + "executeCommand": "", + "tags": { + "title": True, + "artist": True, + "album": True, + "cover": True, + "trackNumber": True, + "trackTotal": False, + "discNumber": True, + "discTotal": False, + "albumArtist": True, + "genre": True, + "year": True, + "date": True, + "explicit": False, + "isrc": True, + "length": True, + "barcode": True, + "bpm": True, + "replayGain": False, + "label": True, + "lyrics": False, + "copyright": False, + "composer": False, + "involvedPeople": False, + "savePlaylistAsCompilation": False, + "useNullSeparator": False, + "saveID3v1": True, + "multiArtistSeparator": "default", + "singleAlbumArtist": False + } + } + + # Create config folder if it doesn't exsist + makedirs(self.configFolder, exist_ok=True) + + # Create config file if it doesn't exsist + if not path.isfile(path.join(self.configFolder, 'config.json')): + with open(path.join(self.configFolder, 'config.json'), 'w') as f: + json.dump(self.defaultSettings, f, indent=2) + + # Read config file + with open(path.join(self.configFolder, 'config.json'), 'r') as configFile: + self.settings = json.load(configFile) + + self.settingsCheck() + + # Make sure the download path exsits + makedirs(self.settings['downloadLocation'], exist_ok=True) + + # LOGFILES + + # Create logfile name and path + logspath = path.join(self.configFolder, 'logs') + now = datetime.datetime.now() + logfile = now.strftime("%Y-%m-%d_%H%M%S")+".log" + makedirs(logspath, exist_ok=True) + + # Add handler for logging + fh = logging.FileHandler(path.join(logspath, logfile)) + fh.setLevel(logging.DEBUG) + fh.setFormatter(logging.Formatter('%(asctime)s - [%(levelname)s] %(message)s')) + logger.addHandler(fh) + logger.info(f"{platform.platform(True, True)} - Python {platform.python_version()}, deemix {deemixVersion}") + + # Only keep last 5 logfiles (to preserve disk space) + logslist = listdir(logspath) + logslist.sort() + if len(logslist)>5: + for i in range(len(logslist)-5): + remove(path.join(logspath, logslist[i])) + + def getSettings(self): + return self.settings + + def getDefaultSettings(self): + return self.defaultSettings + + # Saves the settings + def saveSettings(self, newSettings=None): + if newSettings: + self.settings = newSettings + with open(path.join(self.configFolder, 'config.json'), 'w') as configFile: + json.dump(self.settings, configFile, indent=2) + + # Checks if the default settings have changed + def settingsCheck(self): + changes = 0 + for x in self.defaultSettings: + if not x in self.settings or type(self.settings[x]) != type(self.defaultSettings[x]): + self.settings[x] = self.defaultSettings[x] + changes += 1 + for x in self.defaultSettings['tags']: + if not x in self.settings['tags'] or type(self.settings['tags'][x]) != type(self.defaultSettings['tags'][x]): + self.settings['tags'][x] = self.defaultSettings['tags'][x] + changes += 1 + if self.settings['downloadLocation'] == "": + self.settings['downloadLocation'] = path.join(localpaths.getHomeFolder(), 'deemix Music') + changes += 1 + for template in ['tracknameTemplate', 'albumTracknameTemplate', 'playlistTracknameTemplate', 'playlistNameTemplate', 'artistNameTemplate', 'albumNameTemplate', 'playlistFilenameTemplate', 'coverImageTemplate', 'artistImageTemplate']: + if self.settings[template] == "": + self.settings[template] = self.defaultSettings[template] + changes += 1 + if changes > 0: + saveSettings() diff --git a/deemix/app/__init__.py b/deemix/app/__init__.py index 9d389f1..106d316 100644 --- a/deemix/app/__init__.py +++ b/deemix/app/__init__.py @@ -1,2 +1,6 @@ #!/usr/bin/env python3 # Empty File + +class deemix: + def __init__(self): + diff --git a/deemix/app/default.json b/deemix/app/default.json deleted file mode 100644 index ea927f8..0000000 --- a/deemix/app/default.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "downloadLocation": "", - "tracknameTemplate": "%artist% - %title%", - "albumTracknameTemplate": "%tracknumber% - %title%", - "playlistTracknameTemplate": "%position% - %artist% - %title%", - "createPlaylistFolder": true, - "playlistNameTemplate": "%playlist%", - "createArtistFolder": false, - "artistNameTemplate": "%artist%", - "createAlbumFolder": true, - "albumNameTemplate": "%artist% - %album%", - "createCDFolder": true, - "createStructurePlaylist": false, - "createSingleFolder": false, - "padTracks": true, - "paddingSize": "0", - "illegalCharacterReplacer": "_", - "queueConcurrency": 3, - "maxBitrate": "3", - "fallbackBitrate": true, - "fallbackSearch": false, - "logErrors": true, - "logSearched": false, - "saveDownloadQueue": false, - "overwriteFile": "n", - "createM3U8File": false, - "playlistFilenameTemplate": "playlist", - "syncedLyrics": false, - "embeddedArtworkSize": 800, - "localArtworkSize": 1400, - "localArtworkFormat": "jpg", - "saveArtwork": true, - "coverImageTemplate": "cover", - "saveArtworkArtist": false, - "artistImageTemplate": "folder", - "jpegImageQuality": 80, - "dateFormat": "Y-M-D", - "albumVariousArtists": true, - "removeAlbumVersion": false, - "removeDuplicateArtists": false, - "featuredToTitle": "0", - "titleCasing": "nothing", - "artistCasing": "nothing", - "executeCommand": "", - "tags": { - "title": true, - "artist": true, - "album": true, - "cover": true, - "trackNumber": true, - "trackTotal": false, - "discNumber": true, - "discTotal": false, - "albumArtist": true, - "genre": true, - "year": true, - "date": true, - "explicit": false, - "isrc": true, - "length": true, - "barcode": true, - "bpm": true, - "replayGain": false, - "label": true, - "lyrics": false, - "copyright": false, - "composer": false, - "involvedPeople": false, - "savePlaylistAsCompilation": false, - "useNullSeparator": false, - "saveID3v1": true, - "multiArtistSeparator": "default", - "singleAlbumArtist": false - } -} diff --git a/deemix/app/settings.py b/deemix/app/settings.py deleted file mode 100644 index e55e9b7..0000000 --- a/deemix/app/settings.py +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env python3 -import json -import os.path as path -from os import makedirs, listdir, remove -from deemix import __version__ as deemixVersion -import random -import string -import logging -import datetime -import platform - -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger('deemix') - -import deemix.utils.localpaths as localpaths - -settings = {} -defaultSettings = {} -configDir = "" - -def initSettings(configFolder = None): - global settings - global defaultSettings - global configDir - currentFolder = path.abspath(path.dirname(__file__)) - if not configFolder: - configFolder = localpaths.getConfigFolder() - configDir = configFolder - makedirs(configFolder, exist_ok=True) - with open(path.join(currentFolder, 'default.json'), 'r') as d: - defaultSettings = json.load(d) - defaultSettings['downloadLocation'] = path.join(localpaths.getHomeFolder(), 'deemix Music') - if not path.isfile(path.join(configFolder, 'config.json')): - with open(path.join(configFolder, 'config.json'), 'w') as f: - json.dump(defaultSettings, f, indent=2) - with open(path.join(configFolder, 'config.json'), 'r') as configFile: - settings = json.load(configFile) - settingsCheck() - - if settings['downloadLocation'] == "": - settings['downloadLocation'] = path.join(localpaths.getHomeFolder(), 'deemix Music') - saveSettings(settings) - makedirs(settings['downloadLocation'], exist_ok=True) - - # logfiles - # logfile name - logspath = path.join(configFolder, 'logs') - now = datetime.datetime.now() - logfile = now.strftime("%Y-%m-%d_%H%M%S")+".log" - makedirs(logspath, exist_ok=True) - # add handler for logfile - fh = logging.FileHandler(path.join(logspath, logfile)) - fh.setLevel(logging.DEBUG) - fh.setFormatter(logging.Formatter('%(asctime)s - [%(levelname)s] %(message)s')) - logger.addHandler(fh) - logger.info(f"{platform.platform(True, True)} - Python {platform.python_version()}, deemix {deemixVersion}") - #delete old logfiles - logslist = listdir(logspath) - logslist.sort() - if len(logslist)>5: - for i in range(len(logslist)-5): - remove(path.join(logspath, logslist[i])) - - return settings - - -def getSettings(): - global settings - return settings - - -def getDefaultSettings(): - global defaultSettings - return defaultSettings - - -def saveSettings(newSettings): - global settings - settings = newSettings - with open(path.join(configDir, 'config.json'), 'w') as configFile: - json.dump(settings, configFile, indent=2) - return True - - -def settingsCheck(): - global settings - global defaultSettings - changes = 0 - for x in defaultSettings: - if not x in settings or type(settings[x]) != type(defaultSettings[x]): - settings[x] = defaultSettings[x] - changes += 1 - for x in defaultSettings['tags']: - if not x in settings['tags'] or type(settings['tags'][x]) != type(defaultSettings['tags'][x]): - settings['tags'][x] = defaultSettings['tags'][x] - changes += 1 - if changes > 0: - saveSettings(settings) - - -def randomString(stringLength=8): - letters = string.ascii_lowercase - return ''.join(random.choice(letters) for i in range(stringLength)) diff --git a/deemix/utils/localpaths.py b/deemix/utils/localpaths.py index 5b71e6a..624099d 100644 --- a/deemix/utils/localpaths.py +++ b/deemix/utils/localpaths.py @@ -11,14 +11,12 @@ if getenv("APPDATA"): elif sys.platform.startswith('darwin'): userdata = homedata + '/Library/Application Support/deemix/' elif getenv("XDG_CONFIG_HOME"): - userdata = getenv("XDG_CONFIG_HOME") + '/deemix/'; + userdata = getenv("XDG_CONFIG_HOME") + '/deemix/' else: - userdata = homedata + '/.config/deemix/'; - + userdata = homedata + '/.config/deemix/' def getHomeFolder(): return homedata - def getConfigFolder(): return userdata diff --git a/deemix/utils/misc.py b/deemix/utils/misc.py index 376a674..aa42c0e 100644 --- a/deemix/utils/misc.py +++ b/deemix/utils/misc.py @@ -94,12 +94,3 @@ def uniqueArray(arr): if iPrinc!=iRest and namePrinc.lower() in nRest.lower(): del arr[iRest] return arr - - -def isValidLink(text): - if text.lower().startswith("http"): - if "deezer.com" in text.lower() or "open.spotify.com" in text.lower(): - return True - elif text.lower().startswith("spotify:"): - return True - return False diff --git a/deemix/utils/taggers.py b/deemix/utils/taggers.py index 1cfa420..7f54730 100644 --- a/deemix/utils/taggers.py +++ b/deemix/utils/taggers.py @@ -3,8 +3,9 @@ from mutagen.flac import FLAC, Picture from mutagen.id3 import ID3, ID3NoHeaderError, TXXX, TIT2, TPE1, TALB, TPE2, TRCK, TPOS, TCON, TYER, TDAT, TLEN, TBPM, \ TPUB, TSRC, USLT, APIC, IPLS, TCOM, TCOP, TCMP - +# Adds tags to a MP3 file def tagID3(stream, track, save): + # Delete exsisting tags try: tag = ID3(stream) tag.delete() @@ -13,6 +14,7 @@ def tagID3(stream, track, save): if save['title']: tag.add(TIT2(text=track['title'])) + if save['artist'] and len(track['artists']): if save['multiArtistSeparator'] != "default": if save['multiArtistSeparator'] == "nothing": @@ -22,13 +24,16 @@ def tagID3(stream, track, save): tag.add(TXXX(desc="ARTISTS", text=track['artists'])) else: tag.add(TPE1(text=track['artists'])) + if save['album']: 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'])) else: 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 ""))) @@ -57,6 +62,7 @@ def tagID3(stream, track, save): 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']: if role in ['author', 'engineer', 'mixer', 'producer', 'writer']: @@ -66,10 +72,12 @@ def tagID3(stream, track, save): 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'])) 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: tag.add( @@ -78,13 +86,16 @@ def tagID3(stream, track, save): tag.save(stream, v1=2 if save['saveID3v1'] else 0, v2_version=3, v23_sep=None if save['useNullSeparator'] else '/') - +# Adds tags to a FLAC file def tagFLAC(stream, track, save): + # Delete exsisting tags tag = FLAC(stream) tag.delete() tag.clear_pictures() + if save['title']: tag["TITLE"] = track['title'] + if save['artist'] and len(track['artists']): if save['multiArtistSeparator'] != "default": if save['multiArtistSeparator'] == "nothing": @@ -94,13 +105,16 @@ def tagFLAC(stream, track, save): tag["ARTISTS"] = track['artists'] else: tag["ARTIST"] = track['artists'] + if save['album']: tag["ALBUM"] = track['album']['title'] + if save['albumArtist'] and len(track['album']['artists']): if save['singleAlbumArtist']: tag["ALBUMARTIST"] = track['album']['mainArtist']['name'] else: tag["ALBUMARTIST"] = track['album']['artists'] + if save['trackNumber']: tag["TRACKNUMBER"] = str(track['trackNumber']) if save['trackTotal']: @@ -131,12 +145,14 @@ def tagFLAC(stream, track, save): tag["REPLAYGAIN_TRACK_GAIN"] = track['replayGain'] if 'unsync' in track['lyrics'] and save['lyrics']: tag["LYRICS"] = track['lyrics']['unsync'] + 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] elif role == 'musicpublisher' and save['involvedPeople']: tag["ORGANIZATION"] = track['contributors']['musicpublisher'] + if save['copyright']: tag["COPYRIGHT"] = track['copyright'] if save['savePlaylistAsCompilation'] and "playlist" in track: From c611420bd9bc2146b0a48c846c08f136f23c5984 Mon Sep 17 00:00:00 2001 From: RemixDev Date: Fri, 14 Aug 2020 21:27:50 +0200 Subject: [PATCH 04/13] Started refactoring queuemanager as a class --- deemix/app/queueitem.py | 104 ++++++ deemix/app/queuemanager.py | 676 +++++++++++++++++-------------------- 2 files changed, 423 insertions(+), 357 deletions(-) create mode 100644 deemix/app/queueitem.py diff --git a/deemix/app/queueitem.py b/deemix/app/queueitem.py new file mode 100644 index 0000000..4eccd7e --- /dev/null +++ b/deemix/app/queueitem.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 + +""" +queueItem base structure + title + artist + cover + size + downloaded + failed + errors + progress + type + id + bitrate + uuid: type+id+bitrate +""" + +class QueueItem: + def __init__(self, id=None, bitrate=None, title=None, artist=None, cover=None, size=None, type=None, settings=None, queueItemList=None): + if queueItemList: + self.title = queueItemList['title'] + self.artist = queueItemList['artist'] + self.cover = queueItemList['cover'] + self.size = queueItemList['size'] + self.type = queueItemList['type'] + self.id = queueItemList['id'] + self.bitrate = queueItemList['bitrate'] + self.settings = queueItemList['settings'] + else: + self.title = title + self.artist = artist + self.cover = cover + self.size = size + self.type = type + self.id = id + self.bitrate = bitrate + self.settings = settings + self.downloaded = 0 + self.failed = 0 + self.errors = [] + self.progress = 0 + self.uuid = f"{self.type}_{self.id}_{self.bitrate}" + + def toDict(self): + queueItem = { + 'title': self.title, + 'artist': self.artist, + 'cover': self.cover, + 'size': self.size, + 'downloaded': self.downloaded, + 'failed': self.failed, + 'errors': self.errors, + 'progress': self.progress, + 'type': self.type, + 'id': self.id, + 'bitrate': self.bitrate, + 'uuid': self.uuid + } + return queueItem + + def getResettedItem(self): + item = self.toDict() + item['downloaded'] = 0 + item['failed'] = 0 + item['progress'] = 0 + item['errors'] = [] + return item + + def getSlimmedItem(self): + light = self.toDict() + propertiesToDelete = ['single', 'collection', '_EXTRA'] + for property in propertiesToDelete: + if property in light: + del light[property] + return light + +class QISingle(QueueItem): + def __init__(self, id=None, bitrate=None, title=None, artist=None, cover=None, type=None, settings=None, single=None, queueItemList=None): + if queueItemList: + super().__init__(queueItemList=queueItemList) + self.single = queueItemList['single'] + else: + super().__init__(id, bitrate, title, artist, cover, 1, type, settings) + self.single = single + + def toDict(self): + queueItem = super().toDict() + queueItem['single'] = self.single + return queueItem + +class QICollection(QueueItem): + def __init__(self, id=None, bitrate=None, title=None, artist=None, cover=None, size=None, type=None, settings=None, collection=None, queueItemList=None): + if queueItemList: + super().__init__(queueItemList=queueItemList) + self.collection = queueItemList['collection'] + else: + super().__init__(id, bitrate, title, artist, cover, size, type, settings) + self.collection = collection + + def toDict(self): + queueItem = super().toDict() + queueItem['collection'] = self.collection + return queueItem diff --git a/deemix/app/queuemanager.py b/deemix/app/queuemanager.py index 1bbda4c..989dd41 100644 --- a/deemix/app/queuemanager.py +++ b/deemix/app/queuemanager.py @@ -3,393 +3,344 @@ from deemix.app.downloader import download from deemix.utils.misc import getIDFromLink, getTypeFromLink, getBitrateInt from deemix.api.deezer import APIError from spotipy.exceptions import SpotifyException +from deemix.app.queueitem import QISingle, QICollection import logging import json logging.basicConfig(level=logging.INFO) logger = logging.getLogger('deemix') -queue = [] -queueList = {} -queueComplete = [] -currentItem = "" +class QueueManager: + def __init__(self): + self.queue = [] + self.queueList = {} + self.queueComplete = [] + self.currentItem = "" -""" -queueItem base structure - title - artist - cover - size - downloaded - failed - errors - progress - type - id - bitrate - uuid: type+id+bitrate -if its a single track - single -if its an album/playlist - collection -""" + def generateQueueItem(self, dz, sp, url, settings, bitrate=None, albumAPI=None, interface=None): + forcedBitrate = getBitrateInt(bitrate) + bitrate = forcedBitrate if forcedBitrate else settings['maxBitrate'] + type = getTypeFromLink(url) + id = getIDFromLink(url, type) -def resetQueueItems(items, q): - result = {} - for item in items.keys(): - result[item] = items[item].copy() - if item in q: - result[item]['downloaded'] = 0 - result[item]['failed'] = 0 - result[item]['progress'] = 0 - result[item]['errors'] = [] - return result -def slimQueueItems(items): - result = {} - for item in items.keys(): - result[item] = slimQueueItem(items[item]) - return result + if type == None or id == None: + logger.warn("URL not recognized") + return queueError(url, "URL not recognized", "invalidURL") -def slimQueueItem(item): - light = item.copy() - propertiesToDelete = ['single', 'collection', 'unconverted', '_EXTRA'] - for property in propertiesToDelete: - if property in light: - del light[property] - return light + elif type == "track": + if id.startswith("isrc"): + try: + trackAPI = dz.get_track(id) + if 'id' in trackAPI and 'title' in trackAPI: + id = trackAPI['id'] + else: -def generateQueueItem(dz, sp, url, settings, bitrate=None, albumAPI=None, interface=None): - forcedBitrate = getBitrateInt(bitrate) - bitrate = forcedBitrate if forcedBitrate else settings['maxBitrate'] - type = getTypeFromLink(url) - id = getIDFromLink(url, type) - result = {} - result['link'] = url - if type == None or id == None: - logger.warn("URL not recognized") - result['error'] = "URL not recognized" - result['errid'] = "invalidURL" - elif type == "track": - if id.startswith("isrc"): + except APIError as e: + e = json.loads(str(e)) + return queueError(url, f"Wrong URL: {e['type']+': ' if 'type' in e else ''}{e['message'] if 'message' in e else ''}") try: - trackAPI = dz.get_track(id) - if 'id' in trackAPI and 'title' in trackAPI: - id = trackAPI['id'] - else: - result['error'] = "Track ISRC is not available on deezer" - result['errid'] = "ISRCnotOnDeezer" - return result + trackAPI = dz.get_track_gw(id) except APIError as e: e = json.loads(str(e)) - result['error'] = f"Wrong URL: {e['type']+': ' if 'type' in e else ''}{e['message'] if 'message' in e else ''}" - return result - try: - trackAPI = dz.get_track_gw(id) - except APIError as e: - e = json.loads(str(e)) - result['error'] = "Wrong URL" - if "DATA_ERROR" in e: - result['error'] += f": {e['DATA_ERROR']}" - return result - if albumAPI: - trackAPI['_EXTRA_ALBUM'] = albumAPI - if settings['createSingleFolder']: - trackAPI['FILENAME_TEMPLATE'] = settings['albumTracknameTemplate'] - else: - trackAPI['FILENAME_TEMPLATE'] = settings['tracknameTemplate'] - trackAPI['SINGLE_TRACK'] = True - - result['title'] = trackAPI['SNG_TITLE'] - if 'VERSION' in trackAPI and trackAPI['VERSION']: - result['title'] += " " + trackAPI['VERSION'] - result['artist'] = trackAPI['ART_NAME'] - result[ - 'cover'] = f"https://e-cdns-images.dzcdn.net/images/cover/{trackAPI['ALB_PICTURE']}/75x75-000000-80-0-0.jpg" - result['size'] = 1 - result['downloaded'] = 0 - result['failed'] = 0 - result['errors'] = [] - result['progress'] = 0 - result['type'] = 'track' - result['id'] = id - result['bitrate'] = bitrate - result['uuid'] = f"{result['type']}_{id}_{bitrate}" - result['settings'] = settings or {} - result['single'] = trackAPI - - elif type == "album": - try: - albumAPI = dz.get_album(id) - except APIError as e: - e = json.loads(str(e)) - result['error'] = f"Wrong URL: {e['type']+': ' if 'type' in e else ''}{e['message'] if 'message' in e else ''}" - return result - if id.startswith('upc'): - id = albumAPI['id'] - albumAPI_gw = dz.get_album_gw(id) - albumAPI['nb_disk'] = albumAPI_gw['NUMBER_DISK'] - albumAPI['copyright'] = albumAPI_gw['COPYRIGHT'] - if albumAPI['nb_tracks'] == 1: - return generateQueueItem(dz, sp, f"https://www.deezer.com/track/{albumAPI['tracks']['data'][0]['id']}", - settings, bitrate, albumAPI) - tracksArray = dz.get_album_tracks_gw(id) - if albumAPI['nb_tracks'] == 255: - albumAPI['nb_tracks'] = len(tracksArray) - - result['title'] = albumAPI['title'] - result['artist'] = albumAPI['artist']['name'] - if albumAPI['cover_small'] != None: - result['cover'] = albumAPI['cover_small'][:-24] + '/75x75-000000-80-0-0.jpg' - else: - result['cover'] = f"https://e-cdns-images.dzcdn.net/images/cover/{albumAPI_gw['ALB_PICTURE']}/75x75-000000-80-0-0.jpg" - result['size'] = albumAPI['nb_tracks'] - result['downloaded'] = 0 - result['failed'] = 0 - result['errors'] = [] - result['progress'] = 0 - result['type'] = 'album' - result['id'] = id - result['bitrate'] = bitrate - result['uuid'] = f"{result['type']}_{id}_{bitrate}" - result['settings'] = settings or {} - totalSize = len(tracksArray) - result['collection'] = [] - for pos, trackAPI in enumerate(tracksArray, start=1): - trackAPI['_EXTRA_ALBUM'] = albumAPI - trackAPI['POSITION'] = pos - trackAPI['SIZE'] = totalSize - trackAPI['FILENAME_TEMPLATE'] = settings['albumTracknameTemplate'] - result['collection'].append(trackAPI) - - elif type == "playlist": - try: - playlistAPI = dz.get_playlist(id) - except: - try: - playlistAPI = dz.get_playlist_gw(id)['results']['DATA'] - except APIError as e: - e = json.loads(str(e)) - result['error'] = "Wrong URL" + message = "Wrong URL" if "DATA_ERROR" in e: - result['error'] += f": {e['DATA_ERROR']}" - return result - newPlaylist = { - 'id': playlistAPI['PLAYLIST_ID'], - 'title': playlistAPI['TITLE'], - 'description': playlistAPI['DESCRIPTION'], - 'duration': playlistAPI['DURATION'], - 'public': False, + message += f": {e['DATA_ERROR']}" + return queueError(url, message) + if albumAPI: + trackAPI['_EXTRA_ALBUM'] = albumAPI + if settings['createSingleFolder']: + trackAPI['FILENAME_TEMPLATE'] = settings['albumTracknameTemplate'] + else: + trackAPI['FILENAME_TEMPLATE'] = settings['tracknameTemplate'] + trackAPI['SINGLE_TRACK'] = True + + title = trackAPI['SNG_TITLE'] + if 'VERSION' in trackAPI and trackAPI['VERSION']: + title += " " + trackAPI['VERSION'] + return QISingle( + id, + bitrate, + title, + trackAPI['ART_NAME'], + f"https://e-cdns-images.dzcdn.net/images/cover/{trackAPI['ALB_PICTURE']}/75x75-000000-80-0-0.jpg", + 'track', + settings, + trackAPI, + ) + + elif type == "album": + try: + albumAPI = dz.get_album(id) + except APIError as e: + e = json.loads(str(e)) + return queueError(url, f"Wrong URL: {e['type']+': ' if 'type' in e else ''}{e['message'] if 'message' in e else ''}") + if id.startswith('upc'): + id = albumAPI['id'] + albumAPI_gw = dz.get_album_gw(id) + albumAPI['nb_disk'] = albumAPI_gw['NUMBER_DISK'] + albumAPI['copyright'] = albumAPI_gw['COPYRIGHT'] + if albumAPI['nb_tracks'] == 1: + return generateQueueItem(dz, sp, f"https://www.deezer.com/track/{albumAPI['tracks']['data'][0]['id']}", + settings, bitrate, albumAPI) + tracksArray = dz.get_album_tracks_gw(id) + if albumAPI['nb_tracks'] == 255: + albumAPI['nb_tracks'] = len(tracksArray) + + + if albumAPI['cover_small'] != None: + cover = albumAPI['cover_small'][:-24] + '/75x75-000000-80-0-0.jpg' + else: + cover = f"https://e-cdns-images.dzcdn.net/images/cover/{albumAPI_gw['ALB_PICTURE']}/75x75-000000-80-0-0.jpg" + totalSize = len(tracksArray) + collection = [] + for pos, trackAPI in enumerate(tracksArray, start=1): + trackAPI['_EXTRA_ALBUM'] = albumAPI + trackAPI['POSITION'] = pos + trackAPI['SIZE'] = totalSize + trackAPI['FILENAME_TEMPLATE'] = settings['albumTracknameTemplate'] + collection.append(trackAPI) + + return return QICollection( + id, + bitrate, + albumAPI['title'], + albumAPI['artist']['name'], + cover, + totalSize, + 'album', + settings, + collection, + ) + + + elif type == "playlist": + try: + playlistAPI = dz.get_playlist(id) + except: + try: + playlistAPI = dz.get_playlist_gw(id)['results']['DATA'] + except APIError as e: + e = json.loads(str(e)) + message = "Wrong URL" + if "DATA_ERROR" in e: + message += f": {e['DATA_ERROR']}" + return queueError(url, message) + newPlaylist = { + 'id': playlistAPI['PLAYLIST_ID'], + 'title': playlistAPI['TITLE'], + 'description': playlistAPI['DESCRIPTION'], + 'duration': playlistAPI['DURATION'], + 'public': False, + 'is_loved_track': False, + 'collaborative': False, + 'nb_tracks': playlistAPI['NB_SONG'], + 'fans': playlistAPI['NB_FAN'], + 'link': "https://www.deezer.com/playlist/"+playlistAPI['PLAYLIST_ID'], + 'share': None, + 'picture': "https://api.deezer.com/playlist/"+playlistAPI['PLAYLIST_ID']+"/image", + 'picture_small': "https://cdns-images.dzcdn.net/images/"+playlistAPI['PICTURE_TYPE']+"/"+playlistAPI['PLAYLIST_PICTURE']+"/56x56-000000-80-0-0.jpg", + 'picture_medium': "https://cdns-images.dzcdn.net/images/"+playlistAPI['PICTURE_TYPE']+"/"+playlistAPI['PLAYLIST_PICTURE']+"/250x250-000000-80-0-0.jpg", + 'picture_big': "https://cdns-images.dzcdn.net/images/"+playlistAPI['PICTURE_TYPE']+"/"+playlistAPI['PLAYLIST_PICTURE']+"/500x500-000000-80-0-0.jpg", + 'picture_xl': "https://cdns-images.dzcdn.net/images/"+playlistAPI['PICTURE_TYPE']+"/"+playlistAPI['PLAYLIST_PICTURE']+"/1000x1000-000000-80-0-0.jpg", + 'checksum': playlistAPI['CHECKSUM'], + 'tracklist': "https://api.deezer.com/playlist/"+playlistAPI['PLAYLIST_ID']+"/tracks", + 'creation_date': playlistAPI['DATE_ADD'], + 'creator': { + 'id': playlistAPI['PARENT_USER_ID'], + 'name': playlistAPI['PARENT_USERNAME'], + 'tracklist': "https://api.deezer.com/user/"+playlistAPI['PARENT_USER_ID']+"/flow", + 'type': "user" + }, + 'type': "playlist" + } + playlistAPI = newPlaylist + if not playlistAPI['public'] and playlistAPI['creator']['id'] != str(dz.user['id']): + logger.warn("You can't download others private playlists.") + return return queueError(url, "You can't download others private playlists.", "notYourPrivatePlaylist") + + playlistTracksAPI = dz.get_playlist_tracks_gw(id) + playlistAPI['various_artist'] = dz.get_artist(5080) + + totalSize = len(playlistTracksAPI) + collection = [] + for pos, trackAPI in enumerate(playlistTracksAPI, start=1): + if 'EXPLICIT_TRACK_CONTENT' in trackAPI and 'EXPLICIT_LYRICS_STATUS' in trackAPI['EXPLICIT_TRACK_CONTENT'] and trackAPI['EXPLICIT_TRACK_CONTENT']['EXPLICIT_LYRICS_STATUS'] in [1,4]: + playlistAPI['explicit'] = True + trackAPI['_EXTRA_PLAYLIST'] = playlistAPI + trackAPI['POSITION'] = pos + trackAPI['SIZE'] = totalSize + trackAPI['FILENAME_TEMPLATE'] = settings['playlistTracknameTemplate'] + collection.append(trackAPI) + if not 'explicit' in playlistAPI: + playlistAPI['explicit'] = False + + return return QICollection( + id, + bitrate, + playlistAPI['title'], + playlistAPI['creator']['name'], + playlistAPI['picture_small'][:-24] + '/75x75-000000-80-0-0.jpg', + totalSize, + 'playlist', + settings, + collection, + ) + + elif type == "artist": + try: + artistAPI = dz.get_artist(id) + except APIError as e: + e = json.loads(str(e)) + return return queueError(url, f"Wrong URL: {e['type']+': ' if 'type' in e else ''}{e['message'] if 'message' in e else ''}") + + if interface: + interface.send("startAddingArtist", {'name': artistAPI['name'], 'id': artistAPI['id']}) + + artistAPITracks = dz.get_artist_albums(id) + albumList = [] + for album in artistAPITracks['data']: + albumList.append(generateQueueItem(dz, sp, album['link'], settings, bitrate)) + + if interface: + interface.send("finishAddingArtist", {'name': artistAPI['name'], 'id': artistAPI['id']}) + + return albumList + + elif type == "artistdiscography": + try: + artistAPI = dz.get_artist(id) + except APIError as e: + e = json.loads(str(e)) + return return queueError(url, f"Wrong URL: {e['type']+': ' if 'type' in e else ''}{e['message'] if 'message' in e else ''}") + + if interface: + interface.send("startAddingArtist", {'name': artistAPI['name'], 'id': artistAPI['id']}) + + artistDiscographyAPI = dz.get_artist_discography_gw(id, 100) + albumList = [] + for type in artistDiscographyAPI: + if type != 'all': + for album in artistDiscographyAPI[type]: + albumList.append(generateQueueItem(dz, sp, album['link'], settings, bitrate)) + + if interface: + interface.send("finishAddingArtist", {'name': artistAPI['name'], 'id': artistAPI['id']}) + + return albumList + + elif type == "artisttop": + try: + artistAPI = dz.get_artist(id) + except APIError as e: + e = json.loads(str(e)) + return return queueError(url, f"Wrong URL: {e['type']+': ' if 'type' in e else ''}{e['message'] if 'message' in e else ''}") + + playlistAPI = { + 'id': str(artistAPI['id'])+"_top_track", + 'title': artistAPI['name']+" - Top Tracks", + 'description': "Top Tracks for "+artistAPI['name'], + 'duration': 0, + 'public': True, 'is_loved_track': False, 'collaborative': False, - 'nb_tracks': playlistAPI['NB_SONG'], - 'fans': playlistAPI['NB_FAN'], - 'link': "https://www.deezer.com/playlist/"+playlistAPI['PLAYLIST_ID'], + 'nb_tracks': 0, + 'fans': artistAPI['nb_fan'], + 'link': "https://www.deezer.com/artist/"+str(artistAPI['id'])+"/top_track", 'share': None, - 'picture': "https://api.deezer.com/playlist/"+playlistAPI['PLAYLIST_ID']+"/image", - 'picture_small': "https://cdns-images.dzcdn.net/images/"+playlistAPI['PICTURE_TYPE']+"/"+playlistAPI['PLAYLIST_PICTURE']+"/56x56-000000-80-0-0.jpg", - 'picture_medium': "https://cdns-images.dzcdn.net/images/"+playlistAPI['PICTURE_TYPE']+"/"+playlistAPI['PLAYLIST_PICTURE']+"/250x250-000000-80-0-0.jpg", - 'picture_big': "https://cdns-images.dzcdn.net/images/"+playlistAPI['PICTURE_TYPE']+"/"+playlistAPI['PLAYLIST_PICTURE']+"/500x500-000000-80-0-0.jpg", - 'picture_xl': "https://cdns-images.dzcdn.net/images/"+playlistAPI['PICTURE_TYPE']+"/"+playlistAPI['PLAYLIST_PICTURE']+"/1000x1000-000000-80-0-0.jpg", - 'checksum': playlistAPI['CHECKSUM'], - 'tracklist': "https://api.deezer.com/playlist/"+playlistAPI['PLAYLIST_ID']+"/tracks", - 'creation_date': playlistAPI['DATE_ADD'], + 'picture': artistAPI['picture'], + 'picture_small': artistAPI['picture_small'], + 'picture_medium': artistAPI['picture_medium'], + 'picture_big': artistAPI['picture_big'], + 'picture_xl': artistAPI['picture_xl'], + 'checksum': None, + 'tracklist': "https://api.deezer.com/artist/"+str(artistAPI['id'])+"/top", + 'creation_date': "XXXX-00-00", 'creator': { - 'id': playlistAPI['PARENT_USER_ID'], - 'name': playlistAPI['PARENT_USERNAME'], - 'tracklist': "https://api.deezer.com/user/"+playlistAPI['PARENT_USER_ID']+"/flow", + 'id': "art_"+str(artistAPI['id']), + 'name': artistAPI['name'], 'type': "user" }, 'type': "playlist" } - playlistAPI = newPlaylist - if not playlistAPI['public'] and playlistAPI['creator']['id'] != str(dz.user['id']): - logger.warn("You can't download others private playlists.") - result['error'] = "You can't download others private playlists." - result['errid'] = "notYourPrivatePlaylist" - return result - playlistTracksAPI = dz.get_playlist_tracks_gw(id) - playlistAPI['various_artist'] = dz.get_artist(5080) + artistTopTracksAPI_gw = dz.get_artist_toptracks_gw(id) + playlistAPI['various_artist'] = dz.get_artist(5080) + playlistAPI['nb_tracks'] = len(artistTopTracksAPI_gw) - result['title'] = playlistAPI['title'] - result['artist'] = playlistAPI['creator']['name'] - result['cover'] = playlistAPI['picture_small'][:-24] + '/75x75-000000-80-0-0.jpg' - result['size'] = playlistAPI['nb_tracks'] - result['downloaded'] = 0 - result['failed'] = 0 - result['errors'] = [] - result['progress'] = 0 - result['type'] = 'playlist' - result['id'] = id - result['bitrate'] = bitrate - result['uuid'] = f"{result['type']}_{id}_{bitrate}" - result['settings'] = settings or {} - totalSize = len(playlistTracksAPI) - result['collection'] = [] - for pos, trackAPI in enumerate(playlistTracksAPI, start=1): - if 'EXPLICIT_TRACK_CONTENT' in trackAPI and 'EXPLICIT_LYRICS_STATUS' in trackAPI['EXPLICIT_TRACK_CONTENT'] and trackAPI['EXPLICIT_TRACK_CONTENT']['EXPLICIT_LYRICS_STATUS'] in [1,4]: - playlistAPI['explicit'] = True - trackAPI['_EXTRA_PLAYLIST'] = playlistAPI - trackAPI['POSITION'] = pos - trackAPI['SIZE'] = totalSize - trackAPI['FILENAME_TEMPLATE'] = settings['playlistTracknameTemplate'] - result['collection'].append(trackAPI) - if not 'explicit' in playlistAPI: - playlistAPI['explicit'] = False + totalSize = len(artistTopTracksAPI_gw) + collection = [] + for pos, trackAPI in enumerate(artistTopTracksAPI_gw, start=1): + if 'EXPLICIT_TRACK_CONTENT' in trackAPI and 'EXPLICIT_LYRICS_STATUS' in trackAPI['EXPLICIT_TRACK_CONTENT'] and trackAPI['EXPLICIT_TRACK_CONTENT']['EXPLICIT_LYRICS_STATUS'] in [1,4]: + playlistAPI['explicit'] = True + trackAPI['_EXTRA_PLAYLIST'] = playlistAPI + trackAPI['POSITION'] = pos + trackAPI['SIZE'] = totalSize + trackAPI['FILENAME_TEMPLATE'] = settings['playlistTracknameTemplate'] + collection.append(trackAPI) + if not 'explicit' in playlistAPI: + playlistAPI['explicit'] = False - elif type == "artist": - try: - artistAPI = dz.get_artist(id) - except APIError as e: - e = json.loads(str(e)) - result['error'] = f"Wrong URL: {e['type']+': ' if 'type' in e else ''}{e['message'] if 'message' in e else ''}" - return result - if interface: - interface.send("startAddingArtist", {'name': artistAPI['name'], 'id': artistAPI['id']}) - artistAPITracks = dz.get_artist_albums(id) - albumList = [] - for album in artistAPITracks['data']: - albumList.append(generateQueueItem(dz, sp, album['link'], settings, bitrate)) - if interface: - interface.send("finishAddingArtist", {'name': artistAPI['name'], 'id': artistAPI['id']}) - return albumList - elif type == "artistdiscography": - try: - artistAPI = dz.get_artist(id) - except APIError as e: - e = json.loads(str(e)) - result['error'] = f"Wrong URL: {e['type']+': ' if 'type' in e else ''}{e['message'] if 'message' in e else ''}" - return result - if interface: - interface.send("startAddingArtist", {'name': artistAPI['name'], 'id': artistAPI['id']}) - artistDiscographyAPI = dz.get_artist_discography_gw(id, 100) - albumList = [] - for type in artistDiscographyAPI: - if type != 'all': - for album in artistDiscographyAPI[type]: - albumList.append(generateQueueItem(dz, sp, album['link'], settings, bitrate)) - if interface: - interface.send("finishAddingArtist", {'name': artistAPI['name'], 'id': artistAPI['id']}) - return albumList - elif type == "artisttop": - try: - artistAPI = dz.get_artist(id) - except APIError as e: - e = json.loads(str(e)) - result['error'] = f"Wrong URL: {e['type']+': ' if 'type' in e else ''}{e['message'] if 'message' in e else ''}" - return result + return return QICollection( + id, + bitrate, + playlistAPI['title'], + playlistAPI['creator']['name'], + playlistAPI['picture_small'][:-24] + '/75x75-000000-80-0-0.jpg', + totalSize, + 'playlist', + settings, + collection, + ) - playlistAPI = { - 'id': str(artistAPI['id'])+"_top_track", - 'title': artistAPI['name']+" - Top Tracks", - 'description': "Top Tracks for "+artistAPI['name'], - 'duration': 0, - 'public': True, - 'is_loved_track': False, - 'collaborative': False, - 'nb_tracks': 0, - 'fans': artistAPI['nb_fan'], - 'link': "https://www.deezer.com/artist/"+str(artistAPI['id'])+"/top_track", - 'share': None, - 'picture': artistAPI['picture'], - 'picture_small': artistAPI['picture_small'], - 'picture_medium': artistAPI['picture_medium'], - 'picture_big': artistAPI['picture_big'], - 'picture_xl': artistAPI['picture_xl'], - 'checksum': None, - 'tracklist': "https://api.deezer.com/artist/"+str(artistAPI['id'])+"/top", - 'creation_date': "XXXX-00-00", - 'creator': { - 'id': "art_"+str(artistAPI['id']), - 'name': artistAPI['name'], - 'type': "user" - }, - 'type': "playlist" - } + elif type == "spotifytrack": + if not sp.spotifyEnabled: + logger.warn("Spotify Features is not setted up correctly.") + return queueError(url, "Spotify Features is not setted up correctly.", "spotifyDisabled") - artistTopTracksAPI_gw = dz.get_artist_toptracks_gw(id) - playlistAPI['various_artist'] = dz.get_artist(5080) - playlistAPI['nb_tracks'] = len(artistTopTracksAPI_gw) + try: + track_id = sp.get_trackid_spotify(dz, id, settings['fallbackSearch']) + except SpotifyException as e: + return queueError(url, "Wrong URL: "+e.msg[e.msg.find('\n')+2:]) + + if track_id != 0: + return generateQueueItem(dz, sp, f'https://www.deezer.com/track/{track_id}', settings, bitrate) + else: + logger.warn("Track not found on deezer!") + return queueError(url, "Track not found on deezer!", "trackNotOnDeezer") + + elif type == "spotifyalbum": + if not sp.spotifyEnabled: + logger.warn("Spotify Features is not setted up correctly.") + return queueError(url, "Spotify Features is not setted up correctly.", "spotifyDisabled") + + try: + album_id = sp.get_albumid_spotify(dz, id) + except SpotifyException as e: + return queueError(url, "Wrong URL: "+e.msg[e.msg.find('\n')+2:]) + + if album_id != 0: + return generateQueueItem(dz, sp, f'https://www.deezer.com/album/{album_id}', settings, bitrate) + else: + logger.warn("Album not found on deezer!") + return queueError(url, "Album not found on deezer!", "albumNotOnDeezer") + + elif type == "spotifyplaylist": + if not sp.spotifyEnabled: + logger.warn("Spotify Features is not setted up correctly.") + return queueError(url, "Spotify Features is not setted up correctly.", "spotifyDisabled") + + try: + playlist = sp.adapt_spotify_playlist(dz, id, settings) + playlist['bitrate'] = bitrate + playlist['uuid'] = f"{playlist['type']}_{id}_{bitrate}" + return playlist + except SpotifyException as e: + return queueError(url, "Wrong URL: "+e.msg[e.msg.find('\n')+2:]) - result['title'] = playlistAPI['title'] - result['artist'] = playlistAPI['creator']['name'] - result['cover'] = playlistAPI['picture_small'][:-24] + '/75x75-000000-80-0-0.jpg' - result['size'] = playlistAPI['nb_tracks'] - result['downloaded'] = 0 - result['failed'] = 0 - result['errors'] = [] - result['progress'] = 0 - result['type'] = 'playlist' - result['id'] = id - result['bitrate'] = bitrate - result['uuid'] = f"{result['type']}_{id}_{bitrate}" - result['settings'] = settings or {} - totalSize = len(artistTopTracksAPI_gw) - result['collection'] = [] - for pos, trackAPI in enumerate(artistTopTracksAPI_gw, start=1): - if 'EXPLICIT_TRACK_CONTENT' in trackAPI and 'EXPLICIT_LYRICS_STATUS' in trackAPI['EXPLICIT_TRACK_CONTENT'] and trackAPI['EXPLICIT_TRACK_CONTENT']['EXPLICIT_LYRICS_STATUS'] in [1,4]: - playlistAPI['explicit'] = True - trackAPI['_EXTRA_PLAYLIST'] = playlistAPI - trackAPI['POSITION'] = pos - trackAPI['SIZE'] = totalSize - trackAPI['FILENAME_TEMPLATE'] = settings['playlistTracknameTemplate'] - result['collection'].append(trackAPI) - if not 'explicit' in playlistAPI: - playlistAPI['explicit'] = False - elif type == "spotifytrack": - if not sp.spotifyEnabled: - logger.warn("Spotify Features is not setted up correctly.") - result['error'] = "Spotify Features is not setted up correctly." - result['errid'] = "spotifyDisabled" - return result - try: - track_id = sp.get_trackid_spotify(dz, id, settings['fallbackSearch']) - except SpotifyException as e: - result['error'] = "Wrong URL: "+e.msg[e.msg.find('\n')+2:] - return result - if track_id != 0: - return generateQueueItem(dz, sp, f'https://www.deezer.com/track/{track_id}', settings, bitrate) else: - logger.warn("Track not found on deezer!") - result['error'] = "Track not found on deezer!" - result['errid'] = "trackNotOnDeezer" - elif type == "spotifyalbum": - if not sp.spotifyEnabled: - logger.warn("Spotify Features is not setted up correctly.") - result['error'] = "Spotify Features is not setted up correctly." - result['errid'] = "spotifyDisabled" - return result - try: - album_id = sp.get_albumid_spotify(dz, id) - except SpotifyException as e: - result['error'] = "Wrong URL: "+e.msg[e.msg.find('\n')+2:] - return result - if album_id != 0: - return generateQueueItem(dz, sp, f'https://www.deezer.com/album/{album_id}', settings, bitrate) - else: - logger.warn("Album not found on deezer!") - result['error'] = "Album not found on deezer!" - result['errid'] = "albumNotOnDeezer" - elif type == "spotifyplaylist": - if not sp.spotifyEnabled: - logger.warn("Spotify Features is not setted up correctly.") - result['error'] = "Spotify Features is not setted up correctly." - result['errid'] = "spotifyDisabled" - return result - try: - playlist = sp.adapt_spotify_playlist(dz, id, settings) - except SpotifyException as e: - result['error'] = "Wrong URL: "+e.msg[e.msg.find('\n')+2:] - return result - playlist['bitrate'] = bitrate - playlist['uuid'] = f"{playlist['type']}_{id}_{bitrate}" - result = playlist - else: - logger.warn("URL not supported yet") - result['error'] = "URL not supported yet" - result['errid'] = "unsupportedURL" - return result + logger.warn("URL not supported yet") + return queueError(url, "URL not supported yet", "unsupportedURL") def addToQueue(dz, sp, url, settings, bitrate=None, interface=None): @@ -527,3 +478,14 @@ def removeFinishedDownloads(interface=None): queueComplete = [] if interface: interface.send("removedFinishedDownloads") + +class queueError: + def __init__(self, link, message, errid=None): + self.link = link + self.message = message + self.errid = errid + + def toList(self): + error = { + 'link' + } From 243cf3dfa6cbec31201d5c9c5797df7735536bda Mon Sep 17 00:00:00 2001 From: RemixDev Date: Fri, 14 Aug 2020 22:28:26 +0200 Subject: [PATCH 05/13] Continued code reworking --- deemix/api/deezer.py | 31 +++- deemix/app/queueitem.py | 34 ++-- deemix/app/queuemanager.py | 326 +++++++++++++++++-------------------- deemix/app/spotify.py | 1 + 4 files changed, 194 insertions(+), 198 deletions(-) diff --git a/deemix/api/deezer.py b/deemix/api/deezer.py index e8c25aa..a2153fa 100755 --- a/deemix/api/deezer.py +++ b/deemix/api/deezer.py @@ -257,7 +257,36 @@ class Deezer: return self.gw_api_call('deezer.pageArtist', {'art_id': art_id}) def get_playlist_gw(self, playlist_id): - return self.gw_api_call('deezer.pagePlaylist', {'playlist_id': playlist_id, 'lang': 'en'}) + playlistAPI = self.gw_api_call('deezer.pagePlaylist', {'playlist_id': playlist_id, 'lang': 'en'})['results']['DATA'] + print(json.dumps(playlistAPI)) + return { + 'id': playlistAPI['PLAYLIST_ID'], + 'title': playlistAPI['TITLE'], + 'description': playlistAPI['DESCRIPTION'], + 'duration': playlistAPI['DURATION'], + 'public': playlistAPI['STATUS'] == 1, + 'is_loved_track': playlistAPI['TYPE'] == 4, + 'collaborative': playlistAPI['STATUS'] == 2, + 'nb_tracks': playlistAPI['NB_SONG'], + 'fans': playlistAPI['NB_FAN'], + 'link': "https://www.deezer.com/playlist/"+playlistAPI['PLAYLIST_ID'], + 'share': "https://www.deezer.com/playlist/"+playlistAPI['PLAYLIST_ID'], + 'picture': "https://api.deezer.com/playlist/"+playlistAPI['PLAYLIST_ID']+"/image", + 'picture_small': "https://cdns-images.dzcdn.net/images/"+playlistAPI['PICTURE_TYPE']+"/"+playlistAPI['PLAYLIST_PICTURE']+"/56x56-000000-80-0-0.jpg", + 'picture_medium': "https://cdns-images.dzcdn.net/images/"+playlistAPI['PICTURE_TYPE']+"/"+playlistAPI['PLAYLIST_PICTURE']+"/250x250-000000-80-0-0.jpg", + 'picture_big': "https://cdns-images.dzcdn.net/images/"+playlistAPI['PICTURE_TYPE']+"/"+playlistAPI['PLAYLIST_PICTURE']+"/500x500-000000-80-0-0.jpg", + 'picture_xl': "https://cdns-images.dzcdn.net/images/"+playlistAPI['PICTURE_TYPE']+"/"+playlistAPI['PLAYLIST_PICTURE']+"/1000x1000-000000-80-0-0.jpg", + 'checksum': playlistAPI['CHECKSUM'], + 'tracklist': "https://api.deezer.com/playlist/"+playlistAPI['PLAYLIST_ID']+"/tracks", + 'creation_date': playlistAPI['DATE_ADD'], + 'creator': { + 'id': playlistAPI['PARENT_USER_ID'], + 'name': playlistAPI['PARENT_USERNAME'], + 'tracklist': "https://api.deezer.com/user/"+playlistAPI['PARENT_USER_ID']+"/flow", + 'type': "user" + }, + 'type': "playlist" + } def get_playlist_tracks_gw(self, playlist_id): tracks_array = [] diff --git a/deemix/app/queueitem.py b/deemix/app/queueitem.py index 4eccd7e..b61f61f 100644 --- a/deemix/app/queueitem.py +++ b/deemix/app/queueitem.py @@ -1,21 +1,5 @@ #!/usr/bin/env python3 -""" -queueItem base structure - title - artist - cover - size - downloaded - failed - errors - progress - type - id - bitrate - uuid: type+id+bitrate -""" - class QueueItem: def __init__(self, id=None, bitrate=None, title=None, artist=None, cover=None, size=None, type=None, settings=None, queueItemList=None): if queueItemList: @@ -41,9 +25,10 @@ class QueueItem: self.errors = [] self.progress = 0 self.uuid = f"{self.type}_{self.id}_{self.bitrate}" + self.cancel = False def toDict(self): - queueItem = { + return { 'title': self.title, 'artist': self.artist, 'cover': self.cover, @@ -57,7 +42,6 @@ class QueueItem: 'bitrate': self.bitrate, 'uuid': self.uuid } - return queueItem def getResettedItem(self): item = self.toDict() @@ -102,3 +86,17 @@ class QICollection(QueueItem): queueItem = super().toDict() queueItem['collection'] = self.collection return queueItem + +class QIConvertable(QueueItem): + def __init__(self, id=None, bitrate=None, title=None, artist=None, cover=None, size=None, type=None, settings=None, extra=None, queueItemList=None): + if queueItemList: + super().__init__(queueItemList=queueItemList) + self.extra = queueItemList['_EXTRA'] + else: + super().__init__(id, bitrate, title, artist, cover, size, type, settings) + self.extra = extra + + def toDict(self): + queueItem = super().toDict() + queueItem['_EXTRA'] = self.extra + return queueItem diff --git a/deemix/app/queuemanager.py b/deemix/app/queuemanager.py index 989dd41..d834992 100644 --- a/deemix/app/queuemanager.py +++ b/deemix/app/queuemanager.py @@ -26,7 +26,7 @@ class QueueManager: if type == None or id == None: logger.warn("URL not recognized") - return queueError(url, "URL not recognized", "invalidURL") + return QueueError(url, "URL not recognized", "invalidURL") elif type == "track": if id.startswith("isrc"): @@ -38,7 +38,7 @@ class QueueManager: except APIError as e: e = json.loads(str(e)) - return queueError(url, f"Wrong URL: {e['type']+': ' if 'type' in e else ''}{e['message'] if 'message' in e else ''}") + return QueueError(url, f"Wrong URL: {e['type']+': ' if 'type' in e else ''}{e['message'] if 'message' in e else ''}") try: trackAPI = dz.get_track_gw(id) except APIError as e: @@ -46,7 +46,7 @@ class QueueManager: message = "Wrong URL" if "DATA_ERROR" in e: message += f": {e['DATA_ERROR']}" - return queueError(url, message) + return QueueError(url, message) if albumAPI: trackAPI['_EXTRA_ALBUM'] = albumAPI if settings['createSingleFolder']: @@ -74,7 +74,7 @@ class QueueManager: albumAPI = dz.get_album(id) except APIError as e: e = json.loads(str(e)) - return queueError(url, f"Wrong URL: {e['type']+': ' if 'type' in e else ''}{e['message'] if 'message' in e else ''}") + return QueueError(url, f"Wrong URL: {e['type']+': ' if 'type' in e else ''}{e['message'] if 'message' in e else ''}") if id.startswith('upc'): id = albumAPI['id'] albumAPI_gw = dz.get_album_gw(id) @@ -119,45 +119,16 @@ class QueueManager: playlistAPI = dz.get_playlist(id) except: try: - playlistAPI = dz.get_playlist_gw(id)['results']['DATA'] + playlistAPI = dz.get_playlist_gw(id) except APIError as e: e = json.loads(str(e)) message = "Wrong URL" if "DATA_ERROR" in e: message += f": {e['DATA_ERROR']}" - return queueError(url, message) - newPlaylist = { - 'id': playlistAPI['PLAYLIST_ID'], - 'title': playlistAPI['TITLE'], - 'description': playlistAPI['DESCRIPTION'], - 'duration': playlistAPI['DURATION'], - 'public': False, - 'is_loved_track': False, - 'collaborative': False, - 'nb_tracks': playlistAPI['NB_SONG'], - 'fans': playlistAPI['NB_FAN'], - 'link': "https://www.deezer.com/playlist/"+playlistAPI['PLAYLIST_ID'], - 'share': None, - 'picture': "https://api.deezer.com/playlist/"+playlistAPI['PLAYLIST_ID']+"/image", - 'picture_small': "https://cdns-images.dzcdn.net/images/"+playlistAPI['PICTURE_TYPE']+"/"+playlistAPI['PLAYLIST_PICTURE']+"/56x56-000000-80-0-0.jpg", - 'picture_medium': "https://cdns-images.dzcdn.net/images/"+playlistAPI['PICTURE_TYPE']+"/"+playlistAPI['PLAYLIST_PICTURE']+"/250x250-000000-80-0-0.jpg", - 'picture_big': "https://cdns-images.dzcdn.net/images/"+playlistAPI['PICTURE_TYPE']+"/"+playlistAPI['PLAYLIST_PICTURE']+"/500x500-000000-80-0-0.jpg", - 'picture_xl': "https://cdns-images.dzcdn.net/images/"+playlistAPI['PICTURE_TYPE']+"/"+playlistAPI['PLAYLIST_PICTURE']+"/1000x1000-000000-80-0-0.jpg", - 'checksum': playlistAPI['CHECKSUM'], - 'tracklist': "https://api.deezer.com/playlist/"+playlistAPI['PLAYLIST_ID']+"/tracks", - 'creation_date': playlistAPI['DATE_ADD'], - 'creator': { - 'id': playlistAPI['PARENT_USER_ID'], - 'name': playlistAPI['PARENT_USERNAME'], - 'tracklist': "https://api.deezer.com/user/"+playlistAPI['PARENT_USER_ID']+"/flow", - 'type': "user" - }, - 'type': "playlist" - } - playlistAPI = newPlaylist + return QueueError(url, message) if not playlistAPI['public'] and playlistAPI['creator']['id'] != str(dz.user['id']): logger.warn("You can't download others private playlists.") - return return queueError(url, "You can't download others private playlists.", "notYourPrivatePlaylist") + return return QueueError(url, "You can't download others private playlists.", "notYourPrivatePlaylist") playlistTracksAPI = dz.get_playlist_tracks_gw(id) playlistAPI['various_artist'] = dz.get_artist(5080) @@ -192,7 +163,7 @@ class QueueManager: artistAPI = dz.get_artist(id) except APIError as e: e = json.loads(str(e)) - return return queueError(url, f"Wrong URL: {e['type']+': ' if 'type' in e else ''}{e['message'] if 'message' in e else ''}") + return return QueueError(url, f"Wrong URL: {e['type']+': ' if 'type' in e else ''}{e['message'] if 'message' in e else ''}") if interface: interface.send("startAddingArtist", {'name': artistAPI['name'], 'id': artistAPI['id']}) @@ -212,7 +183,7 @@ class QueueManager: artistAPI = dz.get_artist(id) except APIError as e: e = json.loads(str(e)) - return return queueError(url, f"Wrong URL: {e['type']+': ' if 'type' in e else ''}{e['message'] if 'message' in e else ''}") + return return QueueError(url, f"Wrong URL: {e['type']+': ' if 'type' in e else ''}{e['message'] if 'message' in e else ''}") if interface: interface.send("startAddingArtist", {'name': artistAPI['name'], 'id': artistAPI['id']}) @@ -234,7 +205,7 @@ class QueueManager: artistAPI = dz.get_artist(id) except APIError as e: e = json.loads(str(e)) - return return queueError(url, f"Wrong URL: {e['type']+': ' if 'type' in e else ''}{e['message'] if 'message' in e else ''}") + return return QueueError(url, f"Wrong URL: {e['type']+': ' if 'type' in e else ''}{e['message'] if 'message' in e else ''}") playlistAPI = { 'id': str(artistAPI['id'])+"_top_track", @@ -296,39 +267,39 @@ class QueueManager: elif type == "spotifytrack": if not sp.spotifyEnabled: logger.warn("Spotify Features is not setted up correctly.") - return queueError(url, "Spotify Features is not setted up correctly.", "spotifyDisabled") + return QueueError(url, "Spotify Features is not setted up correctly.", "spotifyDisabled") try: track_id = sp.get_trackid_spotify(dz, id, settings['fallbackSearch']) except SpotifyException as e: - return queueError(url, "Wrong URL: "+e.msg[e.msg.find('\n')+2:]) + return QueueError(url, "Wrong URL: "+e.msg[e.msg.find('\n')+2:]) if track_id != 0: return generateQueueItem(dz, sp, f'https://www.deezer.com/track/{track_id}', settings, bitrate) else: logger.warn("Track not found on deezer!") - return queueError(url, "Track not found on deezer!", "trackNotOnDeezer") + return QueueError(url, "Track not found on deezer!", "trackNotOnDeezer") elif type == "spotifyalbum": if not sp.spotifyEnabled: logger.warn("Spotify Features is not setted up correctly.") - return queueError(url, "Spotify Features is not setted up correctly.", "spotifyDisabled") + return QueueError(url, "Spotify Features is not setted up correctly.", "spotifyDisabled") try: album_id = sp.get_albumid_spotify(dz, id) except SpotifyException as e: - return queueError(url, "Wrong URL: "+e.msg[e.msg.find('\n')+2:]) + return QueueError(url, "Wrong URL: "+e.msg[e.msg.find('\n')+2:]) if album_id != 0: return generateQueueItem(dz, sp, f'https://www.deezer.com/album/{album_id}', settings, bitrate) else: logger.warn("Album not found on deezer!") - return queueError(url, "Album not found on deezer!", "albumNotOnDeezer") + return QueueError(url, "Album not found on deezer!", "albumNotOnDeezer") elif type == "spotifyplaylist": if not sp.spotifyEnabled: logger.warn("Spotify Features is not setted up correctly.") - return queueError(url, "Spotify Features is not setted up correctly.", "spotifyDisabled") + return QueueError(url, "Spotify Features is not setted up correctly.", "spotifyDisabled") try: playlist = sp.adapt_spotify_playlist(dz, id, settings) @@ -336,156 +307,153 @@ class QueueManager: playlist['uuid'] = f"{playlist['type']}_{id}_{bitrate}" return playlist except SpotifyException as e: - return queueError(url, "Wrong URL: "+e.msg[e.msg.find('\n')+2:]) + return QueueError(url, "Wrong URL: "+e.msg[e.msg.find('\n')+2:]) else: logger.warn("URL not supported yet") - return queueError(url, "URL not supported yet", "unsupportedURL") + return QueueError(url, "URL not supported yet", "unsupportedURL") - -def addToQueue(dz, sp, url, settings, bitrate=None, interface=None): - global currentItem, queueList, queue - if not dz.logged_in: - return "Not logged in" - if type(url) is list: - queueItem = [] - for link in url: + def addToQueue(self, dz, sp, url, settings, bitrate=None, interface=None): + if not dz.logged_in: + if interface: + interface.send("loginNeededToDownload") + return False + def parseLink(link): link = link.strip() if link == "": - continue + return False logger.info("Generating queue item for: "+link) - item = generateQueueItem(dz, sp, link, settings, bitrate, interface=interface) - if type(item) is list: - queueItem += item - else: - queueItem.append(item) - else: - url = url.strip() - if url == "": - return False - logger.info("Generating queue item for: "+url) - queueItem = generateQueueItem(dz, sp, url, settings, bitrate, interface=interface) - if type(queueItem) is list: - for x in queueItem: - if 'error' in x: - logger.error(f"[{x['link']}] {x['error']}") - continue - if x['uuid'] in list(queueList.keys()): - logger.warn(f"[{x['uuid']}] Already in queue, will not be added again.") - continue - if interface: - interface.send("addedToQueue", slimQueueItem(x)) - queue.append(x['uuid']) - queueList[x['uuid']] = x - logger.info(f"[{x['uuid']}] Added to queue.") - else: - if 'error' in queueItem: - logger.error(f"[{queueItem['link']}] {queueItem['error']}") - if interface: - interface.send("queueError", queueItem) - return False - if queueItem['uuid'] in list(queueList.keys()): - logger.warn(f"[{queueItem['uuid']}] Already in queue, will not be added again.") - if interface: - interface.send("alreadyInQueue", {'uuid': queueItem['uuid'], 'title': queueItem['title']}) - return False - if interface: - interface.send("addedToQueue", slimQueueItem(queueItem)) - logger.info(f"[{queueItem['uuid']}] Added to queue.") - queue.append(queueItem['uuid']) - queueList[queueItem['uuid']] = queueItem - nextItem(dz, sp, interface) - return True - - -def nextItem(dz, sp, interface=None): - global currentItem, queueList, queue - if currentItem != "": - return None - else: - if len(queue) > 0: - currentItem = queue.pop(0) + return self.generateQueueItem(dz, sp, link, settings, bitrate, interface=interface) + if type(url) is list: + queueItem = [] + for link in url: + item = parseLink(link) + if not item: + continue + elif type(item) is list: + queueItem += item + else: + queueItem.append(item) + if not len(queueItem): + return False else: + queueItem = parseLink(url) + if not queueItem: + return False + if type(queueItem) is list: + ogLen = len(self.queue) + for x in queueItem: + if isinstance(x, QueueError): + logger.error(f"[{x.link}] {x.message}") + continue + if x.uuid in list(self.queueList.keys()): + logger.warn(f"[{x.uuid}] Already in queue, will not be added again.") + continue + self.queue.append(x.uuid) + self.queueList[x.uuid] = x + logger.info(f"[{x.uuid}] Added to queue.") + if ogLen <= len(self.queue): + return False + else: + if isinstance(queueItem, QueueError): + logger.error(f"[{x.link}] {x.message}") + if interface: + interface.send("queueError", queueItem.toDict()) + return False + if queueItem.uuid in list(self.queueList.keys()): + logger.warn(f"[{queueItem.uuid}] Already in queue, will not be added again.") + if interface: + interface.send("alreadyInQueue", {'uuid': queueItem.uuid, 'title': queueItem.title}) + return False + if interface: + interface.send("addedToQueue", queueItem.getSlimmedItem()) + logger.info(f"[{queueItem.uuid}] Added to queue.") + self.queue.append(queueItem.uuid) + self.queueList[queueItem.uuid] = queueItem + self.nextItem(dz, sp, interface) + return True + + def nextItem(self, dz, sp, interface=None): + if self.currentItem != "": return None + else: + if len(self.queue) > 0: + self.currentItem = self.queue.pop(0) + else: + return None + if interface: + interface.send("startDownload", self.currentItem) + logger.info(f"[{self.currentItem}] Started downloading.") + download(dz, sp, self.queueList[self.currentItem], interface) + self.afterDownload(dz, sp, interface) + + def afterDownload(self, dz, sp, interface): + if self.queueList[self.currentItem].cancel: + del self.queueList[self.currentItem] + else: + self.queueComplete.append(self.currentItem) + logger.info(f"[{self.currentItem}] Finished downloading.") + self.currentItem = "" + self.nextItem(dz, sp, interface) + + + def getQueue(self): + return (self.queue, self.queueComplete, self.queueList, self.currentItem) + + # TODO: Convert dicts to QueueItem Objects when restoring + def restoreQueue(self, queue, queueComplete, queueList, dz, sp, interface): + self.queueComplete = queueComplete + self.queueList = queueList + self.queue = queue + nextItem(dz, sp, interface) + + def removeFromQueue(self, uuid, interface=None): + if uuid == self.currentItem: + if interface: + interface.send("cancellingCurrentItem", uuid) + self.queueList[uuid].cancel = True + elif uuid in self.queue: + self.queue.remove(uuid) + del self.queueList[uuid] + if interface: + interface.send("removedFromQueue", uuid) + elif uuid in self.queueComplete: + self.queueComplete.remove(uuid) + del self.queueList[uuid] + if interface: + interface.send("removedFromQueue", uuid) + + + def cancelAllDownloads(self, interface=None): + self.queue = [] + self.queueComplete = [] + if self.currentItem != "": + if interface: + interface.send("cancellingCurrentItem", self.currentItem) + self.queueList[self.currentItem].cancel = True + for uuid in list(self.queueList.keys()): + if uuid != self.currentItem: + del self.queueList[uuid] if interface: - interface.send("startDownload", currentItem) - logger.info(f"[{currentItem}] Started downloading.") - result = download(dz, sp, queueList[currentItem], interface) - callbackQueueDone(result) + interface.send("removedAllDownloads", self.currentItem) -def callbackQueueDone(result): - global currentItem, queueList, queueComplete - if 'cancel' in queueList[currentItem]: - del queueList[currentItem] - else: - queueComplete.append(currentItem) - logger.info(f"[{currentItem}] Finished downloading.") - currentItem = "" - nextItem(result['dz'], result['sp'], result['interface']) - - -def getQueue(): - global currentItem, queueList, queue, queueComplete - return (queue, queueComplete, queueList, currentItem) - - -def restoreQueue(pqueue, pqueueComplete, pqueueList, dz, interface): - global currentItem, queueList, queue, queueComplete - queueComplete = pqueueComplete - queueList = pqueueList - queue = pqueue - nextItem(dz, interface) - - -def removeFromQueue(uuid, interface=None): - global currentItem, queueList, queue, queueComplete - if uuid == currentItem: + def removeFinishedDownloads(self, interface=None): + for uuid in self.queueComplete: + del self.queueList[self.uuid] + self.queueComplete = [] if interface: - interface.send("cancellingCurrentItem", currentItem) - queueList[uuid]['cancel'] = True - elif uuid in queue: - queue.remove(uuid) - del queueList[uuid] - if interface: - interface.send("removedFromQueue", uuid) - elif uuid in queueComplete: - queueComplete.remove(uuid) - del queueList[uuid] - if interface: - interface.send("removedFromQueue", uuid) + interface.send("removedFinishedDownloads") - -def cancelAllDownloads(interface=None): - global currentItem, queueList, queue, queueComplete - queue = [] - queueComplete = [] - if currentItem != "": - if interface: - interface.send("cancellingCurrentItem", currentItem) - queueList[currentItem]['cancel'] = True - for uuid in list(queueList.keys()): - if uuid != currentItem: - del queueList[uuid] - if interface: - interface.send("removedAllDownloads", currentItem) - - -def removeFinishedDownloads(interface=None): - global queueList, queueComplete - for uuid in queueComplete: - del queueList[uuid] - queueComplete = [] - if interface: - interface.send("removedFinishedDownloads") - -class queueError: +class QueueError: def __init__(self, link, message, errid=None): self.link = link self.message = message self.errid = errid - def toList(self): - error = { - 'link' + def toDict(self): + return { + 'link': self.link, + 'error': self.message, + 'errid': self.errid } diff --git a/deemix/app/spotify.py b/deemix/app/spotify.py index 5e2d2f4..8bafc90 100644 --- a/deemix/app/spotify.py +++ b/deemix/app/spotify.py @@ -6,6 +6,7 @@ from os import mkdir import spotipy from spotipy.oauth2 import SpotifyClientCredentials from deemix.utils.localpaths import getConfigFolder +from deemix.app.queueitem import QIConvertable class SpotifyHelper: From 7a536caf1c1f50a3962078f896ccf6a043cb5d00 Mon Sep 17 00:00:00 2001 From: RemixDev Date: Sat, 15 Aug 2020 15:49:45 +0200 Subject: [PATCH 06/13] More code reworking --- deemix/api/deezer.py | 1 - deemix/app/__init__.py | 10 +- deemix/app/downloader.py | 193 +++++++++++++++++----------------- deemix/app/queuemanager.py | 17 ++- deemix/app/spotify.py | 144 ++++++++++++++----------- deemix/utils/pathtemplates.py | 12 +-- 6 files changed, 199 insertions(+), 178 deletions(-) diff --git a/deemix/api/deezer.py b/deemix/api/deezer.py index a2153fa..6cb96bf 100755 --- a/deemix/api/deezer.py +++ b/deemix/api/deezer.py @@ -258,7 +258,6 @@ class Deezer: def get_playlist_gw(self, playlist_id): playlistAPI = self.gw_api_call('deezer.pagePlaylist', {'playlist_id': playlist_id, 'lang': 'en'})['results']['DATA'] - print(json.dumps(playlistAPI)) return { 'id': playlistAPI['PLAYLIST_ID'], 'title': playlistAPI['TITLE'], diff --git a/deemix/app/__init__.py b/deemix/app/__init__.py index 106d316..f721132 100644 --- a/deemix/app/__init__.py +++ b/deemix/app/__init__.py @@ -1,6 +1,12 @@ #!/usr/bin/env python3 -# Empty File +from deemix.api.deezer import Deezer +from deemix.app.settings import Settings +from deemix.app.queuemanager import QueueManager +from deemix.app.spotify import SpotifyHelper class deemix: def __init__(self): - + self.set = Settings() + self.dz = Deezer() + self.sp = SpotifyHelper() + self.qm = QueueManager() diff --git a/deemix/app/downloader.py b/deemix/app/downloader.py index afd108e..4b91cb3 100644 --- a/deemix/app/downloader.py +++ b/deemix/app/downloader.py @@ -15,6 +15,7 @@ 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 @@ -41,7 +42,7 @@ lastPercentage = 0 def stream_track(dz, track, stream, trackAPI, queueItem, interface=None): global downloadPercentage, lastPercentage - if 'cancel' in queueItem: + if queueItem.cancel: raise downloadCancelled try: request = get(track['downloadUrl'], headers=dz.http_headers, stream=True, timeout=30) @@ -55,7 +56,7 @@ def stream_track(dz, track, stream, trackAPI, queueItem, interface=None): percentage = 0 i = 0 for chunk in request.iter_content(2048): - if 'cancel' in queueItem: + 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) @@ -69,9 +70,9 @@ def stream_track(dz, track, stream, trackAPI, queueItem, interface=None): downloadPercentage += chunkProgres if round(downloadPercentage) != lastPercentage and round(downloadPercentage) % 2 == 0: lastPercentage = round(downloadPercentage) - queueItem['progress'] = lastPercentage + queueItem.progress = lastPercentage if interface: - interface.send("updateQueue", {'uuid': queueItem['uuid'], 'progress': lastPercentage}) + interface.send("updateQueue", {'uuid': queueItem.uuid, 'progress': lastPercentage}) i += 1 @@ -83,9 +84,9 @@ def trackCompletePercentage(trackAPI, queueItem, interface): downloadPercentage += 1 / trackAPI['SIZE'] * 100 if round(downloadPercentage) != lastPercentage and round(downloadPercentage) % 2 == 0: lastPercentage = round(downloadPercentage) - queueItem['progress'] = lastPercentage + queueItem.progress = lastPercentage if interface: - interface.send("updateQueue", {'uuid': queueItem['uuid'], 'progress': lastPercentage}) + interface.send("updateQueue", {'uuid': queueItem.uuid, 'progress': lastPercentage}) def trackRemovePercentage(trackAPI, queueItem, interface): global downloadPercentage, lastPercentage @@ -95,9 +96,9 @@ def trackRemovePercentage(trackAPI, queueItem, interface): downloadPercentage -= 1 / trackAPI['SIZE'] * 100 if round(downloadPercentage) != lastPercentage and round(downloadPercentage) % 2 == 0: lastPercentage = round(downloadPercentage) - queueItem['progress'] = lastPercentage + queueItem.progress = lastPercentage if interface: - interface.send("updateQueue", {'uuid': queueItem['uuid'], 'progress': lastPercentage}) + interface.send("updateQueue", {'uuid': queueItem.uuid, 'progress': lastPercentage}) def downloadImage(url, path, overwrite="n"): @@ -474,12 +475,45 @@ def getTrackData(dz, trackAPI_gw, settings, trackAPI=None, albumAPI_gw=None, alb 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 'cancel' in queueItem: + if queueItem.cancel: result['cancel'] = True return result @@ -495,10 +529,10 @@ def downloadTrackObj(dz, trackAPI, settings, bitrate, queueItem, extraTrack=None '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']) + queueItem.failed += 1 + queueItem.errors.append(result['error']) if interface: - interface.send("updateQueue", {'uuid': queueItem['uuid'], 'failed': True, 'data': result['error']['data'], + 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 @@ -512,7 +546,7 @@ def downloadTrackObj(dz, trackAPI, settings, bitrate, queueItem, extraTrack=None trackAPI=trackAPI['_EXTRA_TRACK'] if '_EXTRA_TRACK' in trackAPI else None, albumAPI=trackAPI['_EXTRA_ALBUM'] if '_EXTRA_ALBUM' in trackAPI else None ) - if 'cancel' in queueItem: + if queueItem.cancel: result['cancel'] = True return result if track['MD5'] == '': @@ -545,10 +579,10 @@ def downloadTrackObj(dz, trackAPI, settings, bitrate, queueItem, extraTrack=None 'artist': track['mainArtist']['name'] } } - queueItem['failed'] += 1 - queueItem['errors'].append(result['error']) + queueItem.failed += 1 + queueItem.errors.append(result['error']) if interface: - interface.send("updateQueue", {'uuid': queueItem['uuid'], 'failed': True, 'data': result['error']['data'], + interface.send("updateQueue", {'uuid': queueItem.uuid, 'failed': True, 'data': result['error']['data'], 'error': result['error']['message'], 'errid': result['error']['errid']}) return result else: @@ -563,10 +597,10 @@ def downloadTrackObj(dz, trackAPI, settings, bitrate, queueItem, extraTrack=None 'artist': track['mainArtist']['name'] } } - queueItem['failed'] += 1 - queueItem['errors'].append(result['error']) + queueItem.failed += 1 + queueItem.errors.append(result['error']) if interface: - interface.send("updateQueue", {'uuid': queueItem['uuid'], 'failed': True, 'data': result['error']['data'], + interface.send("updateQueue", {'uuid': queueItem.uuid, 'failed': True, 'data': result['error']['data'], 'error': result['error']['message'], 'errid': result['error']['errid']}) return result @@ -602,10 +636,10 @@ def downloadTrackObj(dz, trackAPI, settings, bitrate, queueItem, extraTrack=None 'artist': track['mainArtist']['name'] } } - queueItem['failed'] += 1 - queueItem['errors'].append(result['error']) + queueItem.failed += 1 + queueItem.errors.append(result['error']) if interface: - interface.send("updateQueue", {'uuid': queueItem['uuid'], 'failed': True, 'data': result['error']['data'], + interface.send("updateQueue", {'uuid': queueItem.uuid, 'failed': True, 'data': result['error']['data'], 'error': result['error']['message'], 'errid': result['error']['errid']}) return result else: @@ -620,10 +654,10 @@ def downloadTrackObj(dz, trackAPI, settings, bitrate, queueItem, extraTrack=None 'artist': track['mainArtist']['name'] } } - queueItem['failed'] += 1 - queueItem['errors'].append(result['error']) + queueItem.failed += 1 + queueItem.errors.append(result['error']) if interface: - interface.send("updateQueue", {'uuid': queueItem['uuid'], 'failed': True, 'data': result['error']['data'], + 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: @@ -638,45 +672,13 @@ def downloadTrackObj(dz, trackAPI, settings, bitrate, queueItem, extraTrack=None 'artist': track['mainArtist']['name'] } } - queueItem['failed'] += 1 - queueItem['errors'].append(result['error']) + queueItem.failed += 1 + queueItem.errors.append(result['error']) if interface: - interface.send("updateQueue", {'uuid': queueItem['uuid'], 'failed': True, 'data': result['error']['data'], + 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 "_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" if settings['tags']['savePlaylistAsCompilation'] and "playlist" in track: track['trackNumber'] = trackAPI["POSITION"] track['discNumber'] = "1" @@ -732,7 +734,7 @@ def downloadTrackObj(dz, trackAPI, settings, bitrate, queueItem, extraTrack=None filename = generateFilename(track, trackAPI, settings) (filepath, artistPath, coverPath, extrasPath) = generateFilepath(track, trackAPI, settings) - if 'cancel' in queueItem: + if queueItem.cancel: result['cancel'] = True return result # Download and cache coverart @@ -865,10 +867,10 @@ def downloadTrackObj(dz, trackAPI, settings, bitrate, queueItem, extraTrack=None 'artist': track['mainArtist']['name'] } } - queueItem['failed'] += 1 - queueItem['errors'].append(result['error']) + queueItem.failed += 1 + queueItem.errors.append(result['error']) if interface: - interface.send("updateQueue", {'uuid': queueItem['uuid'], 'failed': True, 'data': result['error']['data'], + interface.send("updateQueue", {'uuid': queueItem.uuid, 'failed': True, 'data': result['error']['data'], 'error': result['error']['message'], 'errid': result['error']['errid']}) return 1 else: @@ -883,10 +885,10 @@ def downloadTrackObj(dz, trackAPI, settings, bitrate, queueItem, extraTrack=None 'artist': track['mainArtist']['name'] } } - queueItem['failed'] += 1 - queueItem['errors'].append(result['error']) + queueItem.failed += 1 + queueItem.errors.append(result['error']) if interface: - interface.send("updateQueue", {'uuid': queueItem['uuid'], 'failed': True, 'data': result['error']['data'], + 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: @@ -919,9 +921,9 @@ def downloadTrackObj(dz, trackAPI, settings, bitrate, queueItem, extraTrack=None 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 + queueItem.downloaded += 1 if interface: - interface.send("updateQueue", {'uuid': queueItem['uuid'], 'downloaded': True, 'downloadPath': writepath}) + interface.send("updateQueue", {'uuid': queueItem.uuid, 'downloaded': True, 'downloadPath': writepath}) return result @@ -939,61 +941,56 @@ def downloadTrackObj_wrap(dz, track, settings, bitrate, queueItem, interface): } } } - queueItem['failed'] += 1 - queueItem['errors'].append(result['error']) + queueItem.failed += 1 + queueItem.errors.append(result['error']) if interface: - interface.send("updateQueue", {'uuid': queueItem['uuid'], 'failed': True, 'data': result['error']['data'], + 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'] + settings = queueItem.settings + bitrate = queueItem.bitrate downloadPercentage = 0 lastPercentage = 0 - if '_EXTRA' in queueItem: + if isinstance(queueItem, QIConvertable): sp.convert_spotify_playlist(dz, queueItem, settings, interface=interface) - if 'single' in queueItem: + if isinstance(queueItem, QISingle): try: - result = downloadTrackObj(dz, queueItem['single'], settings, bitrate, queueItem, interface=interface) + 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']} + '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']) + queueItem.failed += 1 + queueItem.errors.append(result['error']) if interface: - interface.send("updateQueue", {'uuid': queueItem['uuid'], 'failed': True, 'data': result['error']['data'], + interface.send("updateQueue", {'uuid': queueItem.uuid, 'failed': True, 'data': result['error']['data'], 'error': result['error']['message']}) - download_path = after_download_single(result, settings, queueItem) - elif 'collection' in queueItem: - playlist = [None] * len(queueItem['collection']) + 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): + 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 'cancel' in queueItem: - interface.send('currentItemCancelled', queueItem['uuid']) - interface.send("removedFromQueue", queueItem['uuid']) + if queueItem.cancel: + interface.send('currentItemCancelled', queueItem.uuid) + interface.send("removedFromQueue", queueItem.uuid) else: - interface.send("finishDownload", queueItem['uuid']) - return { - 'dz': dz, - 'sp': sp, - 'interface': interface, - 'download_path': download_path - } + interface.send("finishDownload", queueItem.uuid) + return download_path def after_download(tracks, settings, queueItem): @@ -1049,7 +1046,7 @@ def after_download(tracks, settings, queueItem): return extrasPath -def after_download_single(track, settings, queueItem): +def after_download_single(track, settings): if 'cancel' in track: return None if 'extrasPath' not in track: diff --git a/deemix/app/queuemanager.py b/deemix/app/queuemanager.py index d834992..d5252d8 100644 --- a/deemix/app/queuemanager.py +++ b/deemix/app/queuemanager.py @@ -101,7 +101,7 @@ class QueueManager: trackAPI['FILENAME_TEMPLATE'] = settings['albumTracknameTemplate'] collection.append(trackAPI) - return return QICollection( + return QICollection( id, bitrate, albumAPI['title'], @@ -128,7 +128,7 @@ class QueueManager: return QueueError(url, message) if not playlistAPI['public'] and playlistAPI['creator']['id'] != str(dz.user['id']): logger.warn("You can't download others private playlists.") - return return QueueError(url, "You can't download others private playlists.", "notYourPrivatePlaylist") + return QueueError(url, "You can't download others private playlists.", "notYourPrivatePlaylist") playlistTracksAPI = dz.get_playlist_tracks_gw(id) playlistAPI['various_artist'] = dz.get_artist(5080) @@ -146,7 +146,7 @@ class QueueManager: if not 'explicit' in playlistAPI: playlistAPI['explicit'] = False - return return QICollection( + return QICollection( id, bitrate, playlistAPI['title'], @@ -163,7 +163,7 @@ class QueueManager: artistAPI = dz.get_artist(id) except APIError as e: e = json.loads(str(e)) - return return QueueError(url, f"Wrong URL: {e['type']+': ' if 'type' in e else ''}{e['message'] if 'message' in e else ''}") + return QueueError(url, f"Wrong URL: {e['type']+': ' if 'type' in e else ''}{e['message'] if 'message' in e else ''}") if interface: interface.send("startAddingArtist", {'name': artistAPI['name'], 'id': artistAPI['id']}) @@ -183,7 +183,7 @@ class QueueManager: artistAPI = dz.get_artist(id) except APIError as e: e = json.loads(str(e)) - return return QueueError(url, f"Wrong URL: {e['type']+': ' if 'type' in e else ''}{e['message'] if 'message' in e else ''}") + return QueueError(url, f"Wrong URL: {e['type']+': ' if 'type' in e else ''}{e['message'] if 'message' in e else ''}") if interface: interface.send("startAddingArtist", {'name': artistAPI['name'], 'id': artistAPI['id']}) @@ -205,7 +205,7 @@ class QueueManager: artistAPI = dz.get_artist(id) except APIError as e: e = json.loads(str(e)) - return return QueueError(url, f"Wrong URL: {e['type']+': ' if 'type' in e else ''}{e['message'] if 'message' in e else ''}") + return QueueError(url, f"Wrong URL: {e['type']+': ' if 'type' in e else ''}{e['message'] if 'message' in e else ''}") playlistAPI = { 'id': str(artistAPI['id'])+"_top_track", @@ -252,7 +252,7 @@ class QueueManager: if not 'explicit' in playlistAPI: playlistAPI['explicit'] = False - return return QICollection( + return QICollection( id, bitrate, playlistAPI['title'], @@ -302,9 +302,8 @@ class QueueManager: return QueueError(url, "Spotify Features is not setted up correctly.", "spotifyDisabled") try: - playlist = sp.adapt_spotify_playlist(dz, id, settings) + playlist = sp.generate_playlist_queueitem(dz, id, settings) playlist['bitrate'] = bitrate - playlist['uuid'] = f"{playlist['type']}_{id}_{bitrate}" return playlist except SpotifyException as e: return QueueError(url, "Wrong URL: "+e.msg[e.msg.find('\n')+2:]) diff --git a/deemix/app/spotify.py b/deemix/app/spotify.py index 8bafc90..406441b 100644 --- a/deemix/app/spotify.py +++ b/deemix/app/spotify.py @@ -6,43 +6,45 @@ from os import mkdir import spotipy from spotipy.oauth2 import SpotifyClientCredentials from deemix.utils.localpaths import getConfigFolder -from deemix.app.queueitem import QIConvertable +from deemix.app.queueitem import QIConvertable, QICollection +emptyPlaylist = { + 'collaborative': False, + 'description': "", + 'external_urls': {'spotify': None}, + 'followers': {'total': 0, 'href': None}, + 'id': None, + 'images': [], + 'name': "Something went wrong", + 'owner': { + 'display_name': "Error", + 'id': None + }, + 'public': True, + 'tracks' : [], + 'type': 'playlist', + 'uri': None +} class SpotifyHelper: def __init__(self, configFolder=None): self.credentials = {} self.spotifyEnabled = False self.sp = None - if not configFolder: - self.configFolder = getConfigFolder() - else: - self.configFolder = configFolder - self.emptyPlaylist = { - 'collaborative': False, - 'description': "", - 'external_urls': {'spotify': None}, - 'followers': {'total': 0, 'href': None}, - 'id': None, - 'images': [], - 'name': "Something went wrong", - 'owner': { - 'display_name': "Error", - 'id': None - }, - 'public': True, - 'tracks' : [], - 'type': 'playlist', - 'uri': None - } - self.initCredentials() + self.configFolder = configFolder - def initCredentials(self): + # Make sure config folder exsists + if not self.configFolder: + self.configFolder = getConfigFolder() if not path.isdir(self.configFolder): mkdir(self.configFolder) + + # Make sure authCredentials exsits if not path.isfile(path.join(self.configFolder, 'authCredentials.json')): with open(path.join(self.configFolder, 'authCredentials.json'), 'w') as f: json.dump({'clientId': "", 'clientSecret': ""}, f, indent=2) + + # Load spotify id and secret and check if they are usable with open(path.join(self.configFolder, 'authCredentials.json'), 'r') as credentialsFile: self.credentials = json.load(credentialsFile) self.checkCredentials() @@ -52,7 +54,9 @@ class SpotifyHelper: spotifyEnabled = False else: try: - self.createSpotifyConnection() + client_credentials_manager = SpotifyClientCredentials(client_id=self.credentials['clientId'], + client_secret=self.credentials['clientSecret']) + self.sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) self.sp.user_playlists('spotify') self.spotifyEnabled = True except Exception as e: @@ -63,18 +67,19 @@ class SpotifyHelper: return self.credentials def setCredentials(self, spotifyCredentials): + # Remove extra spaces, just to be sure spotifyCredentials['clientId'] = spotifyCredentials['clientId'].strip() spotifyCredentials['clientSecret'] = spotifyCredentials['clientSecret'].strip() + + # Save them to disk with open(path.join(self.configFolder, 'authCredentials.json'), 'w') as f: json.dump(spotifyCredentials, f, indent=2) + + # Check if they are usable self.credentials = spotifyCredentials self.checkCredentials() - def createSpotifyConnection(self): - client_credentials_manager = SpotifyClientCredentials(client_id=self.credentials['clientId'], - client_secret=self.credentials['clientSecret']) - self.sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager) - + # Converts spotify API playlist structure to deezer's playlist structure def _convert_playlist_structure(self, spotify_obj): if len(spotify_obj['images']): url = spotify_obj['images'][0]['url'] @@ -115,6 +120,7 @@ class SpotifyHelper: deezer_obj['picture_xl'] = "https://e-cdns-images.dzcdn.net/images/cover/d41d8cd98f00b204e9800998ecf8427e/1000x1000-000000-80-0-0.jpg" return deezer_obj + # Returns deezer song_id from spotify track_id or track dict def get_trackid_spotify(self, dz, track_id, fallbackSearch, spotifyTrack=None): if not self.spotifyEnabled: raise spotifyFeaturesNotEnabled @@ -148,6 +154,7 @@ class SpotifyHelper: json.dump(cache, spotifyCache) return dz_track + # Returns deezer album_id from spotify album_id def get_albumid_spotify(self, dz, album_id): if not self.spotifyEnabled: raise spotifyFeaturesNotEnabled @@ -175,33 +182,24 @@ class SpotifyHelper: json.dump(cache, spotifyCache) return dz_album - def adapt_spotify_playlist(self, dz, playlist_id, settings): + + def generate_playlist_queueitem(self, dz, playlist_id, settings): if not self.spotifyEnabled: raise spotifyFeaturesNotEnabled spotify_playlist = self.sp.playlist(playlist_id) - result = { - 'title': spotify_playlist['name'], - 'artist': spotify_playlist['owner']['display_name'], - 'size': spotify_playlist['tracks']['total'], - 'downloaded': 0, - 'failed': 0, - 'progress': 0, - 'errors': [], - 'type': 'spotify_playlist', - 'settings': settings or {}, - 'id': playlist_id - } + if len(spotify_playlist['images']): - result['cover'] = spotify_playlist['images'][0]['url'] + cover = spotify_playlist['images'][0]['url'] else: - result[ - 'cover'] = "https://e-cdns-images.dzcdn.net/images/cover/d41d8cd98f00b204e9800998ecf8427e/75x75-000000-80-0-0.jpg" + cover = "https://e-cdns-images.dzcdn.net/images/cover/d41d8cd98f00b204e9800998ecf8427e/75x75-000000-80-0-0.jpg" + playlistAPI = self._convert_playlist_structure(spotify_playlist) playlistAPI['various_artist'] = dz.get_artist(5080) + + extra = {} + extra['unconverted'] = [] + tracklistTmp = spotify_playlist['tracks']['items'] - result['collection'] = [] - result['_EXTRA'] = {} - result['_EXTRA']['unconverted'] = [] while spotify_playlist['tracks']['next']: spotify_playlist['tracks'] = self.sp.next(spotify_playlist['tracks']) tracklistTmp += spotify_playlist['tracks']['items'] @@ -209,13 +207,23 @@ class SpotifyHelper: if item['track']: if item['track']['explicit']: playlistAPI['explicit'] = True - result['_EXTRA']['unconverted'].append(item['track']) - totalSize = len(result['_EXTRA']['unconverted']) - result['size'] = totalSize + extra['unconverted'].append(item['track']) + + totalSize = len(extra['unconverted']) if not 'explicit' in playlistAPI: playlistAPI['explicit'] = False - result['_EXTRA']['playlistAPI'] = playlistAPI - return result + extra['playlistAPI'] = playlistAPI + return QICollection( + playlist_id, + 0, + spotify_playlist['name'], + spotify_playlist['owner']['display_name'], + cover, + totalSize, + 'spotify_playlist', + settings, + extra, + ) def convert_spotify_playlist(self, dz, item, settings, interface=None): convertPercentage = 0 @@ -226,8 +234,9 @@ class SpotifyHelper: else: cache = {'tracks': {}, 'albums': {}} if interface: - interface.send("startConversion", item['uuid']) - for pos, track in enumerate(item['_EXTRA']['unconverted'], start=1): + interface.send("startConversion", item.uuid) + collection = [] + for pos, track in enumerate(item.extra['unconverted'], start=1): if str(track['id']) in cache['tracks']: trackID = cache['tracks'][str(track['id'])] else: @@ -248,20 +257,31 @@ class SpotifyHelper: } else: deezerTrack = dz.get_track_gw(trackID) - deezerTrack['_EXTRA_PLAYLIST'] = item['_EXTRA']['playlistAPI'] + deezerTrack['_EXTRA_PLAYLIST'] = item.extra['playlistAPI'] deezerTrack['POSITION'] = pos - deezerTrack['SIZE'] = item['size'] + deezerTrack['SIZE'] = item.size deezerTrack['FILENAME_TEMPLATE'] = settings['playlistTracknameTemplate'] - item['collection'].append(deezerTrack) + collection.append(deezerTrack) - convertPercentage = (pos / item['size']) * 100 + convertPercentage = (pos / item.size) * 100 print(convertPercentage) if round(convertPercentage) != lastPercentage and round(convertPercentage) % 2 == 0: lastPercentage = round(convertPercentage) if interface: - interface.send("updateQueue", {'uuid': item['uuid'], 'conversion': lastPercentage}) + interface.send("updateQueue", {'uuid': item.uuid, 'conversion': lastPercentage}) - del item['_EXTRA'] + item = QICollection( + item.id, + item.bitrate, + item.title, + item.artist, + item.cover, + item.size, + item.type, + item.settings, + collection, + ) + with open(path.join(self.configFolder, 'spotifyCache.json'), 'w') as spotifyCache: json.dump(cache, spotifyCache) if interface: diff --git a/deemix/utils/pathtemplates.py b/deemix/utils/pathtemplates.py index 7b21ef6..cffcb83 100644 --- a/deemix/utils/pathtemplates.py +++ b/deemix/utils/pathtemplates.py @@ -218,11 +218,11 @@ def settingsRegexPlaylist(foldername, playlist, settings): return antiDot(fixLongName(foldername)) def settingsRegexPlaylistFile(foldername, queueItem, settings): - foldername = foldername.replace("%title%", fixName(queueItem['title'], settings['illegalCharacterReplacer'])) - foldername = foldername.replace("%artist%", fixName(queueItem['artist'], settings['illegalCharacterReplacer'])) - foldername = foldername.replace("%size%", str(queueItem['size'])) - foldername = foldername.replace("%type%", fixName(queueItem['type'], settings['illegalCharacterReplacer'])) - foldername = foldername.replace("%id%", fixName(queueItem['id'], settings['illegalCharacterReplacer'])) - foldername = foldername.replace("%bitrate%", bitrateLabels[int(queueItem['bitrate'])]) + foldername = foldername.replace("%title%", fixName(queueItem.title, settings['illegalCharacterReplacer'])) + foldername = foldername.replace("%artist%", fixName(queueItem.artist, settings['illegalCharacterReplacer'])) + foldername = foldername.replace("%size%", str(queueItem.size)) + foldername = foldername.replace("%type%", fixName(queueItem.type, settings['illegalCharacterReplacer'])) + foldername = foldername.replace("%id%", fixName(queueItem.id, settings['illegalCharacterReplacer'])) + foldername = foldername.replace("%bitrate%", bitrateLabels[int(queueItem.bitrate)]) foldername = foldername.replace('\\', pathSep).replace('/', pathSep).replace(pathSep, settings['illegalCharacterReplacer']) return antiDot(fixLongName(foldername)) From 4cfdc4872dd1d308ec045f2557f5383052d8876a Mon Sep 17 00:00:00 2001 From: RemixDev Date: Sat, 15 Aug 2020 21:34:10 +0200 Subject: [PATCH 07/13] 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) From 34263c150f2d491c378a5eb1a0900caea27d1920 Mon Sep 17 00:00:00 2001 From: RemixDev Date: Sat, 15 Aug 2020 23:03:05 +0200 Subject: [PATCH 08/13] Made the refactoring work --- deemix/__main__.py | 19 +---- deemix/api/deezer.py | 62 ++++++++-------- deemix/app/__init__.py | 8 +- deemix/app/cli.py | 70 +++++++++--------- deemix/app/{DownloadJob.py => downloadjob.py} | 73 +++++++++---------- ...essageInterface.py => messageinterface.py} | 0 deemix/app/queueitem.py | 56 +++++++------- .../app/{QueueManager.py => queuemanager.py} | 12 ++- deemix/app/{Settings.py => settings.py} | 0 .../{SpotifyHelper.py => spotifyhelper.py} | 38 ++++------ deemix/app/{Track.py => track.py} | 14 ++-- deemix/utils/misc.py | 1 - deemix/utils/pathtemplates.py | 4 +- deemix/utils/taggers.py | 4 +- 14 files changed, 171 insertions(+), 190 deletions(-) rename deemix/app/{DownloadJob.py => downloadjob.py} (90%) rename deemix/app/{MessageInterface.py => messageinterface.py} (100%) rename deemix/app/{QueueManager.py => queuemanager.py} (98%) rename deemix/app/{Settings.py => settings.py} (100%) rename deemix/app/{SpotifyHelper.py => spotifyhelper.py} (92%) rename deemix/app/{Track.py => track.py} (98%) diff --git a/deemix/__main__.py b/deemix/__main__.py index df63226..ed2533b 100644 --- a/deemix/__main__.py +++ b/deemix/__main__.py @@ -1,36 +1,25 @@ #!/usr/bin/env python3 import click -import deemix.app.cli as app -from deemix.app.settings import initSettings +from deemix.app.cli import cli from os.path import isfile -import random -import string - -def randomString(stringLength=8): - letters = string.ascii_lowercase - return ''.join(random.choice(letters) for i in range(stringLength)) - @click.command() @click.option('-b', '--bitrate', default=None, help='Overwrites the default bitrate selected') @click.option('-l', '--local', is_flag=True, help='Downloads in a local folder insted of using the default') @click.argument('url', nargs=-1, required=True) def download(bitrate, local, url): - settings = initSettings() - if local: - settings['downloadLocation'] = randomString(12) - click.echo("Using a local download folder: "+settings['downloadLocation']) + app = cli(local) app.login() url = list(url) if isfile(url[0]): filename = url[0] with open(filename) as f: url = f.readlines() - app.downloadLink(url, settings, bitrate) + app.downloadLink(url, bitrate) click.echo("All done!") if local: - click.echo(settings['downloadLocation']) #folder name output + click.echo(app.set.settings['downloadLocation']) #folder name output if __name__ == '__main__': download() diff --git a/deemix/api/deezer.py b/deemix/api/deezer.py index 6cb96bf..e0e09de 100755 --- a/deemix/api/deezer.py +++ b/deemix/api/deezer.py @@ -477,37 +477,37 @@ class Deezer: for track in data: item = { 'id': track['SNG_ID'], - 'title': track['SNG_TITLE'], - 'link': 'https://www.deezer.com/track/'+str(track['SNG_ID']), - 'duration': track['DURATION'], - 'rank': track['RANK_SNG'], - 'explicit_lyrics': int(track['EXPLICIT_LYRICS']) > 0, - 'explicit_content_lyrics': track['EXPLICIT_TRACK_CONTENT']['EXPLICIT_COVER_STATUS'], - 'explicit_content_cover': track['EXPLICIT_TRACK_CONTENT']['EXPLICIT_LYRICS_STATUS'], - 'time_add': track['DATE_ADD'], - 'album': { - 'id': track['ALB_ID'], - 'title': track['ALB_TITLE'], - 'cover': 'https://api.deezer.com/album/'+str(track['ALB_ID'])+'/image', - 'cover_small': 'https://e-cdns-images.dzcdn.net/images/cover/'+str(track['ALB_PICTURE'])+'/56x56-000000-80-0-0.jpg', - 'cover_medium': 'https://e-cdns-images.dzcdn.net/images/cover/'+str(track['ALB_PICTURE'])+'/250x250-000000-80-0-0.jpg', - 'cover_big': 'https://e-cdns-images.dzcdn.net/images/cover/'+str(track['ALB_PICTURE'])+'/500x500-000000-80-0-0.jpg', - 'cover_xl': 'https://e-cdns-images.dzcdn.net/images/cover/'+str(track['ALB_PICTURE'])+'/1000x1000-000000-80-0-0.jpg', - 'tracklist': 'https://api.deezer.com/album/'+str(track['ALB_ID'])+'/tracks', - 'type': 'album' - }, - 'artist': { - 'id': track['ART_ID'], - 'name': track['ART_NAME'], - 'picture': 'https://api.deezer.com/artist/'+str(track['ART_ID'])+'/image', - 'picture_small': 'https://e-cdns-images.dzcdn.net/images/artist/'+str(track['ART_PICTURE'])+'/56x56-000000-80-0-0.jpg', - 'picture_medium': 'https://e-cdns-images.dzcdn.net/images/artist/'+str(track['ART_PICTURE'])+'/250x250-000000-80-0-0.jpg', - 'picture_big': 'https://e-cdns-images.dzcdn.net/images/artist/'+str(track['ART_PICTURE'])+'/500x500-000000-80-0-0.jpg', - 'picture_xl': 'https://e-cdns-images.dzcdn.net/images/artist/'+str(track['ART_PICTURE'])+'/1000x1000-000000-80-0-0.jpg', - 'tracklist': 'https://api.deezer.com/artist/'+str(track['ART_ID'])+'/top?limit=50', - 'type': 'artist' - }, - 'type': 'track' + 'title': track['SNG_TITLE'], + 'link': 'https://www.deezer.com/track/'+str(track['SNG_ID']), + 'duration': track['DURATION'], + 'rank': track['RANK_SNG'], + 'explicit_lyrics': int(track['EXPLICIT_LYRICS']) > 0, + 'explicit_content_lyrics': track['EXPLICIT_TRACK_CONTENT']['EXPLICIT_COVER_STATUS'], + 'explicit_content_cover': track['EXPLICIT_TRACK_CONTENT']['EXPLICIT_LYRICS_STATUS'], + 'time_add': track['DATE_ADD'], + 'album': { + 'id': track['ALB_ID'], + 'title': track['ALB_TITLE'], + 'cover': 'https://api.deezer.com/album/'+str(track['ALB_ID'])+'/image', + 'cover_small': 'https://e-cdns-images.dzcdn.net/images/cover/'+str(track['ALB_PICTURE'])+'/56x56-000000-80-0-0.jpg', + 'cover_medium': 'https://e-cdns-images.dzcdn.net/images/cover/'+str(track['ALB_PICTURE'])+'/250x250-000000-80-0-0.jpg', + 'cover_big': 'https://e-cdns-images.dzcdn.net/images/cover/'+str(track['ALB_PICTURE'])+'/500x500-000000-80-0-0.jpg', + 'cover_xl': 'https://e-cdns-images.dzcdn.net/images/cover/'+str(track['ALB_PICTURE'])+'/1000x1000-000000-80-0-0.jpg', + 'tracklist': 'https://api.deezer.com/album/'+str(track['ALB_ID'])+'/tracks', + 'type': 'album' + }, + 'artist': { + 'id': track['ART_ID'], + 'name': track['ART_NAME'], + 'picture': 'https://api.deezer.com/artist/'+str(track['ART_ID'])+'/image', + 'picture_small': 'https://e-cdns-images.dzcdn.net/images/artist/'+str(track['ART_PICTURE'])+'/56x56-000000-80-0-0.jpg', + 'picture_medium': 'https://e-cdns-images.dzcdn.net/images/artist/'+str(track['ART_PICTURE'])+'/250x250-000000-80-0-0.jpg', + 'picture_big': 'https://e-cdns-images.dzcdn.net/images/artist/'+str(track['ART_PICTURE'])+'/500x500-000000-80-0-0.jpg', + 'picture_xl': 'https://e-cdns-images.dzcdn.net/images/artist/'+str(track['ART_PICTURE'])+'/1000x1000-000000-80-0-0.jpg', + 'tracklist': 'https://api.deezer.com/artist/'+str(track['ART_ID'])+'/top?limit=50', + 'type': 'artist' + }, + 'type': 'track' } result.append(item) return result diff --git a/deemix/app/__init__.py b/deemix/app/__init__.py index f721132..d126811 100644 --- a/deemix/app/__init__.py +++ b/deemix/app/__init__.py @@ -2,11 +2,11 @@ from deemix.api.deezer import Deezer from deemix.app.settings import Settings from deemix.app.queuemanager import QueueManager -from deemix.app.spotify import SpotifyHelper +from deemix.app.spotifyhelper import SpotifyHelper class deemix: - def __init__(self): - self.set = Settings() + def __init__(self, configFolder=None): + self.set = Settings(configFolder) self.dz = Deezer() - self.sp = SpotifyHelper() + self.sp = SpotifyHelper(configFolder) self.qm = QueueManager() diff --git a/deemix/app/cli.py b/deemix/app/cli.py index f2812c8..0bba93f 100644 --- a/deemix/app/cli.py +++ b/deemix/app/cli.py @@ -1,43 +1,47 @@ #!/usr/bin/env python3 import os.path as path +import string +import random from os import mkdir -from deemix.utils import localpaths -from deemix.api.deezer import Deezer -from deemix.app.queuemanager import addToQueue -from deemix.app.spotify import SpotifyHelper +from deemix.app import deemix -dz = Deezer() -sp = SpotifyHelper() +def randomString(stringLength=8): + letters = string.ascii_lowercase + return ''.join(random.choice(letters) for i in range(stringLength)) +class cli(deemix): + def __init__(self, local, configFolder=None): + super().__init__(configFolder) + if local: + self.set.settings['downloadLocation'] = randomString(12) + print("Using a local download folder: "+settings['downloadLocation']) -def requestValidArl(): - while True: - arl = input("Paste here your arl:") - if dz.login_via_arl(arl): - break - return arl + def downloadLink(self, url, bitrate=None): + for link in url: + if ';' in link: + for l in link.split(";"): + self.qm.addToQueue(self.dz, self.sp, l, self.set.settings, bitrate) + else: + self.qm.addToQueue(self.dz, self.sp, link, self.set.settings, bitrate) + def requestValidArl(self): + while True: + arl = input("Paste here your arl:") + if self.dz.login_via_arl(arl): + break + return arl -def login(): - configFolder = localpaths.getConfigFolder() - if not path.isdir(configFolder): - mkdir(configFolder) - if path.isfile(path.join(configFolder, '.arl')): - with open(path.join(configFolder, '.arl'), 'r') as f: - arl = f.readline().rstrip("\n") - if not dz.login_via_arl(arl): - arl = requestValidArl() - else: - arl = requestValidArl() - with open(path.join(configFolder, '.arl'), 'w') as f: - f.write(arl) - - -def downloadLink(url, settings, bitrate=None): - for link in url: - if ';' in link: - for l in link.split(";"): - addToQueue(dz, sp, l, settings, bitrate) + def login(self): + configFolder = self.set.configFolder + if not path.isdir(configFolder): + mkdir(configFolder) + if path.isfile(path.join(configFolder, '.arl')): + with open(path.join(configFolder, '.arl'), 'r') as f: + arl = f.readline().rstrip("\n") + if not self.dz.login_via_arl(arl): + arl = self.requestValidArl() else: - addToQueue(dz, sp, link, settings, bitrate) + arl = self.requestValidArl() + with open(path.join(configFolder, '.arl'), 'w') as f: + f.write(arl) diff --git a/deemix/app/DownloadJob.py b/deemix/app/downloadjob.py similarity index 90% rename from deemix/app/DownloadJob.py rename to deemix/app/downloadjob.py index 33d8f15..8a3adc8 100644 --- a/deemix/app/DownloadJob.py +++ b/deemix/app/downloadjob.py @@ -11,7 +11,7 @@ from tempfile import gettempdir from time import sleep from deemix.app.queueitem import QIConvertable, QISingle, QICollection -from deemix.app.Track import Track +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 @@ -46,8 +46,8 @@ errorMessages = { '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!", + '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): @@ -168,8 +168,10 @@ class DownloadJob: def __init__(self, dz, sp, queueItem, interface=None): self.dz = dz self.sp = sp - self.queueItem = queueItem self.interface = interface + if isinstance(queueItem, QIConvertable): + self.sp.convert_spotify_playlist(self.dz, queueItem, interface=self.interface) + self.queueItem = queueItem self.settings = queueItem.settings self.bitrate = queueItem.bitrate self.downloadPercentage = 0 @@ -177,8 +179,6 @@ class DownloadJob: 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: @@ -215,15 +215,15 @@ class DownloadJob: ) if self.queueItem.cancel: raise DownloadCancelled - if self.MD5 == '': + if track.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']) + logger.warn(f"[{track.mainArtist['name']} - {track.title}] Track not yet encoded, searching for alternative") + searchedId = self.dz.get_track_from_metadata(track.mainArtist['name'], track.title, track.album['title']) if searchedId != 0: newTrack = self.dz.get_track_gw(searchedId) track.parseEssentialData(self.dz, newTrack) @@ -242,8 +242,8 @@ class DownloadJob: 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']) + logger.warn(f"[{track.mainArtist['name']} - {track.title}] Track not found at desired bitrate, searching for alternative") + searchedId = self.dz.get_track_from_metadata(track.mainArtist['name'], track.title, track.album['title']) if searchedId != 0: newTrack = self.dz.get_track_gw(searchedId) track.parseEssentialData(self.dz, newTrack) @@ -261,8 +261,7 @@ class DownloadJob: 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") + track.album['picPath'] = os.path.join(TEMPDIR, f"pl{trackAPI_gw['_EXTRA_PLAYLIST']['id']}_{self.settings['embeddedArtworkSize']}.jpg") else: if track.album['date']: track.date = track.album['date'] @@ -271,10 +270,11 @@ class DownloadJob: self.settings['embeddedArtworkSize'], self.settings['embeddedArtworkSize'], f'000000-{self.settings["jpegImageQuality"]}-0-0.jpg' ) + track.album['picPath'] = os.path.join(TEMPDIR, f"alb{track.album['id']}_{self.settings['embeddedArtworkSize']}.jpg") track.album['bitrate'] = selectedFormat - track.dateString = formatDate(track.date, settings['dateFormat']) - track.album['dateString'] = formatDate(track.album['date'], settings['dateFormat']) + track.dateString = formatDate(track.date, self.settings['dateFormat']) + track.album['dateString'] = formatDate(track.album['date'], self.settings['dateFormat']) # Check if user wants the feat in the title # 0 => do not change @@ -320,11 +320,6 @@ class DownloadJob: 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']) @@ -418,7 +413,7 @@ class DownloadJob: 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) + track.downloadUrl = self.dz.get_track_stream_url(track.id, track.MD5, track.mediaVersion, track.selectedFormat) def downloadMusic(track, trackAPI_gw): try: @@ -435,8 +430,8 @@ class DownloadJob: 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']) + logger.warn(f"[{track.mainArtist['name']} - {track.title}] Track not available, searching for alternative") + searchedId = self.dz.get_track_from_metadata(track.mainArtist['name'], track.title, track.album['title']) if searchedId != 0: newTrack = self.dz.get_track_gw(searchedId) track.parseEssentialData(self.dz, newTrack) @@ -460,7 +455,7 @@ class DownloadJob: try: trackDownloaded = downloadMusic(track, trackAPI_gw) except DownloadFailed as e: - raise DownloadFailed + raise e except Exception as e: raise e @@ -468,7 +463,7 @@ class DownloadJob: 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) + self.completeTrackPercentage() # Adding tags if (not trackAlreadyDownloaded or self.settings['overwriteFile'] in ['t', 'y']) and not track.localTrack: @@ -482,10 +477,10 @@ class DownloadJob: 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" + track.filesizes['FILESIZE_FLAC'] = "0" return self.download(trackAPI_gw, track) if track.searched: - result['searched'] = f'{track.mainArtist['name']} - {track.title}' + result['searched'] = f"{track.mainArtist['name']} - {track.title}" logger.info(f"[{track.mainArtist['name']} - {track.title}] Track download completed") self.queueItem.downloaded += 1 @@ -533,16 +528,16 @@ class DownloadJob: return error_num # fallback is enabled and loop went through all formats - def stream_track(self, stream, track, trackAPI): + def streamTrack(self, stream, track, trackAPI): if self.queueItem.cancel: raise DownloadCancelled try: - request = get(track.downloadUrl, headers=dz.http_headers, stream=True, timeout=30) + request = get(track.downloadUrl, headers=self.dz.http_headers, stream=True, timeout=30) except ConnectionError: sleep(2) - return stream_track(dz, track, stream, trackAPI, queueItem, interface) + return self.streamTrack(stream, track, trackAPI) request.raise_for_status() - blowfish_key = str.encode(dz._get_blowfish_key(str(track.id))) + blowfish_key = str.encode(self.dz._get_blowfish_key(str(track.id))) complete = int(request.headers["Content-Length"]) chunkLength = 0 percentage = 0 @@ -585,9 +580,9 @@ class DownloadJob: 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']} + 'id': trackAPI_gw['SNG_ID'], + 'title': trackAPI_gw['SNG_TITLE'] + (trackAPI_gw['VERSION'] if 'VERSION' in trackAPI_gw and trackAPI_gw['VERSION'] and not trackAPI_gw['VERSION'] in trackAPI_gw['SNG_TITLE'] else ""), + 'artist': trackAPI_gw['ART_NAME'] } try: @@ -595,14 +590,14 @@ class DownloadJob: except DownloadCancelled: return None except DownloadFailed as error: - logger.error(f"[{track['mainArtist']['name']} - {track['title']}] {error.message}") + logger.error(f"[{track['artist']} - {track['title']}] {error.message}") result = {'error': { 'message': error.message, 'errid': error.errid, 'data': track }} except Exception as e: - logger.exception(str(e)) + logger.exception(f"[{track['artist']} - {track['title']}] {str(e)}") result = {'error': { 'message': str(e), 'data': track @@ -611,10 +606,10 @@ class DownloadJob: if 'error' in result: self.completeTrackPercentage() self.queueItem.failed += 1 - self.queueItem.errors.append(error.message) - if interface: + self.queueItem.errors.append(result['error']['message']) + if self.interface: error = result['error'] - interface.send("updateQueue", { + self.interface.send("updateQueue", { 'uuid': self.queueItem.uuid, 'failed': True, 'data': error['data'], diff --git a/deemix/app/MessageInterface.py b/deemix/app/messageinterface.py similarity index 100% rename from deemix/app/MessageInterface.py rename to deemix/app/messageinterface.py diff --git a/deemix/app/queueitem.py b/deemix/app/queueitem.py index b61f61f..7023715 100644 --- a/deemix/app/queueitem.py +++ b/deemix/app/queueitem.py @@ -4,43 +4,43 @@ class QueueItem: def __init__(self, id=None, bitrate=None, title=None, artist=None, cover=None, size=None, type=None, settings=None, queueItemList=None): if queueItemList: self.title = queueItemList['title'] - self.artist = queueItemList['artist'] - self.cover = queueItemList['cover'] - self.size = queueItemList['size'] - self.type = queueItemList['type'] - self.id = queueItemList['id'] - self.bitrate = queueItemList['bitrate'] + self.artist = queueItemList['artist'] + self.cover = queueItemList['cover'] + self.size = queueItemList['size'] + self.type = queueItemList['type'] + self.id = queueItemList['id'] + self.bitrate = queueItemList['bitrate'] self.settings = queueItemList['settings'] else: self.title = title - self.artist = artist - self.cover = cover - self.size = size - self.type = type - self.id = id - self.bitrate = bitrate + self.artist = artist + self.cover = cover + self.size = size + self.type = type + self.id = id + self.bitrate = bitrate self.settings = settings - self.downloaded = 0 - self.failed = 0 + self.downloaded = 0 + self.failed = 0 self.errors = [] - self.progress = 0 - self.uuid = f"{self.type}_{self.id}_{self.bitrate}" + self.progress = 0 + self.uuid = f"{self.type}_{self.id}_{self.bitrate}" self.cancel = False def toDict(self): return { 'title': self.title, - 'artist': self.artist, - 'cover': self.cover, - 'size': self.size, - 'downloaded': self.downloaded, - 'failed': self.failed, + 'artist': self.artist, + 'cover': self.cover, + 'size': self.size, + 'downloaded': self.downloaded, + 'failed': self.failed, 'errors': self.errors, - 'progress': self.progress, - 'type': self.type, - 'id': self.id, - 'bitrate': self.bitrate, - 'uuid': self.uuid + 'progress': self.progress, + 'type': self.type, + 'id': self.id, + 'bitrate': self.bitrate, + 'uuid': self.uuid } def getResettedItem(self): @@ -87,13 +87,13 @@ class QICollection(QueueItem): queueItem['collection'] = self.collection return queueItem -class QIConvertable(QueueItem): +class QIConvertable(QICollection): def __init__(self, id=None, bitrate=None, title=None, artist=None, cover=None, size=None, type=None, settings=None, extra=None, queueItemList=None): if queueItemList: super().__init__(queueItemList=queueItemList) self.extra = queueItemList['_EXTRA'] else: - super().__init__(id, bitrate, title, artist, cover, size, type, settings) + super().__init__(id, bitrate, title, artist, cover, size, type, settings, []) self.extra = extra def toDict(self): diff --git a/deemix/app/QueueManager.py b/deemix/app/queuemanager.py similarity index 98% rename from deemix/app/QueueManager.py rename to deemix/app/queuemanager.py index d5252d8..d78e8db 100644 --- a/deemix/app/QueueManager.py +++ b/deemix/app/queuemanager.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -from deemix.app.downloader import download +from deemix.app.downloadjob import DownloadJob from deemix.utils.misc import getIDFromLink, getTypeFromLink, getBitrateInt from deemix.api.deezer import APIError from spotipy.exceptions import SpotifyException @@ -35,7 +35,7 @@ class QueueManager: if 'id' in trackAPI and 'title' in trackAPI: id = trackAPI['id'] else: - + return QueueError(url, "Track ISRC is not available on deezer", "ISRCnotOnDeezer") except APIError as e: e = json.loads(str(e)) return QueueError(url, f"Wrong URL: {e['type']+': ' if 'type' in e else ''}{e['message'] if 'message' in e else ''}") @@ -303,7 +303,7 @@ class QueueManager: try: playlist = sp.generate_playlist_queueitem(dz, id, settings) - playlist['bitrate'] = bitrate + playlist.bitrate = bitrate return playlist except SpotifyException as e: return QueueError(url, "Wrong URL: "+e.msg[e.msg.find('\n')+2:]) @@ -317,12 +317,14 @@ class QueueManager: if interface: interface.send("loginNeededToDownload") return False + def parseLink(link): link = link.strip() if link == "": return False logger.info("Generating queue item for: "+link) return self.generateQueueItem(dz, sp, link, settings, bitrate, interface=interface) + if type(url) is list: queueItem = [] for link in url: @@ -339,6 +341,7 @@ class QueueManager: queueItem = parseLink(url) if not queueItem: return False + if type(queueItem) is list: ogLen = len(self.queue) for x in queueItem: @@ -369,6 +372,7 @@ class QueueManager: logger.info(f"[{queueItem.uuid}] Added to queue.") self.queue.append(queueItem.uuid) self.queueList[queueItem.uuid] = queueItem + self.nextItem(dz, sp, interface) return True @@ -383,7 +387,7 @@ class QueueManager: if interface: interface.send("startDownload", self.currentItem) logger.info(f"[{self.currentItem}] Started downloading.") - download(dz, sp, self.queueList[self.currentItem], interface) + DownloadJob(dz, sp, self.queueList[self.currentItem]).start() self.afterDownload(dz, sp, interface) def afterDownload(self, dz, sp, interface): diff --git a/deemix/app/Settings.py b/deemix/app/settings.py similarity index 100% rename from deemix/app/Settings.py rename to deemix/app/settings.py diff --git a/deemix/app/SpotifyHelper.py b/deemix/app/spotifyhelper.py similarity index 92% rename from deemix/app/SpotifyHelper.py rename to deemix/app/spotifyhelper.py index 406441b..ae7cafe 100644 --- a/deemix/app/SpotifyHelper.py +++ b/deemix/app/spotifyhelper.py @@ -213,7 +213,7 @@ class SpotifyHelper: if not 'explicit' in playlistAPI: playlistAPI['explicit'] = False extra['playlistAPI'] = playlistAPI - return QICollection( + return QIConvertable( playlist_id, 0, spotify_playlist['name'], @@ -225,7 +225,7 @@ class SpotifyHelper: extra, ) - def convert_spotify_playlist(self, dz, item, settings, interface=None): + def convert_spotify_playlist(self, dz, queueItem, interface=None): convertPercentage = 0 lastPercentage = 0 if path.isfile(path.join(self.configFolder, 'spotifyCache.json')): @@ -234,13 +234,13 @@ class SpotifyHelper: else: cache = {'tracks': {}, 'albums': {}} if interface: - interface.send("startConversion", item.uuid) + interface.send("startConversion", queueItem.uuid) collection = [] - for pos, track in enumerate(item.extra['unconverted'], start=1): + for pos, track in enumerate(queueItem.extra['unconverted'], start=1): if str(track['id']) in cache['tracks']: trackID = cache['tracks'][str(track['id'])] else: - trackID = self.get_trackid_spotify(dz, 0, settings['fallbackSearch'], track) + trackID = self.get_trackid_spotify(dz, 0, queueItem.settings['fallbackSearch'], track) cache['tracks'][str(track['id'])] = trackID if trackID == 0: deezerTrack = { @@ -257,35 +257,25 @@ class SpotifyHelper: } else: deezerTrack = dz.get_track_gw(trackID) - deezerTrack['_EXTRA_PLAYLIST'] = item.extra['playlistAPI'] + deezerTrack['_EXTRA_PLAYLIST'] = queueItem.extra['playlistAPI'] deezerTrack['POSITION'] = pos - deezerTrack['SIZE'] = item.size - deezerTrack['FILENAME_TEMPLATE'] = settings['playlistTracknameTemplate'] + deezerTrack['SIZE'] = queueItem.size + deezerTrack['FILENAME_TEMPLATE'] = queueItem.settings['playlistTracknameTemplate'] collection.append(deezerTrack) - convertPercentage = (pos / item.size) * 100 - print(convertPercentage) + convertPercentage = (pos / queueItem.size) * 100 if round(convertPercentage) != lastPercentage and round(convertPercentage) % 2 == 0: lastPercentage = round(convertPercentage) if interface: - interface.send("updateQueue", {'uuid': item.uuid, 'conversion': lastPercentage}) + interface.send("updateQueue", {'uuid': queueItem.uuid, 'conversion': lastPercentage}) + + queueItem.extra = None + queueItem.collection = collection - item = QICollection( - item.id, - item.bitrate, - item.title, - item.artist, - item.cover, - item.size, - item.type, - item.settings, - collection, - ) - with open(path.join(self.configFolder, 'spotifyCache.json'), 'w') as spotifyCache: json.dump(cache, spotifyCache) if interface: - interface.send("startDownload", item['uuid']) + interface.send("startDownload", queueItem.uuid) def get_user_playlists(self, user): if not self.spotifyEnabled: diff --git a/deemix/app/Track.py b/deemix/app/track.py similarity index 98% rename from deemix/app/Track.py rename to deemix/app/track.py index d7e20cd..c9826d6 100644 --- a/deemix/app/Track.py +++ b/deemix/app/track.py @@ -13,7 +13,7 @@ class Track: 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.title += " " + trackAPI_gw['VERSION'].strip() self.position = None if 'POSITION' in trackAPI_gw: @@ -87,7 +87,7 @@ class Track: self.album['bitrate'] = 0 self.album['dateString'] = None - self.artistsString + self.artistsString = "" def parseEssentialData(self, dz, trackAPI_gw): self.id = trackAPI_gw['SNG_ID'] @@ -97,7 +97,7 @@ class Track: self.fallbackId = "0" if 'FALLBACK' in trackAPI_gw: self.fallbackId = trackAPI_gw['FALLBACK']['SNG_ID'] - self.formats = dz.get_track_filesizes(track["id"]) + self.filesizes = dz.get_track_filesizes(self.id) def parseLocalTrackData(self, trackAPI_gw): self.album = { @@ -160,7 +160,7 @@ class Track: self.trackNumber = trackAPI_gw['TRACK_NUMBER'] self.contributors = trackAPI_gw['SNG_CONTRIBUTORS'] - track.lyrics = { + self.lyrics = { 'id': None, 'unsync': None, 'sync': None @@ -184,13 +184,13 @@ class Track: self.lyrics['sync'] += lastTimestamp self.lyrics['sync'] += trackAPI_gw["LYRICS"]["LYRICS_SYNC_JSON"][i]["line"] + "\r\n" - track.mainArtist = { + self.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.mainArtist['pic'] = trackAPI_gw['ART_PICTURE'] self.date = None if 'PHYSICAL_RELEASE_DATE' in trackAPI_gw: @@ -261,7 +261,7 @@ class Track: if 'copyright' in albumAPI: self.copyright = albumAPI['copyright'] - if not track.album['pic']: + if not self.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: diff --git a/deemix/utils/misc.py b/deemix/utils/misc.py index e30520f..2c57a4a 100644 --- a/deemix/utils/misc.py +++ b/deemix/utils/misc.py @@ -50,7 +50,6 @@ def andCommaConcat(lst): result = "" for i, art in enumerate(lst): result += art - track['commaArtistsString'] += art if tot != i + 1: if tot - 1 == i + 1: result += " & " diff --git a/deemix/utils/pathtemplates.py b/deemix/utils/pathtemplates.py index 96259cd..aef5ef7 100644 --- a/deemix/utils/pathtemplates.py +++ b/deemix/utils/pathtemplates.py @@ -130,10 +130,10 @@ def generateFilepath(track, trackAPI, settings): 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("%artists%", fixName(", ".join(track.artists), 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("%featartists%", fixName('('+track.featArtistsString+')', settings['illegalCharacterReplacer']) if track.featArtistsString else "") filename = filename.replace("%album%", fixName(track.album['title'], settings['illegalCharacterReplacer'])) filename = filename.replace("%albumartist%", fixName(track.album['mainArtist']['name'], settings['illegalCharacterReplacer'])) diff --git a/deemix/utils/taggers.py b/deemix/utils/taggers.py index 26f63df..8c4fc96 100644 --- a/deemix/utils/taggers.py +++ b/deemix/utils/taggers.py @@ -75,7 +75,7 @@ def tagID3(stream, track, save): if save['copyright']: tag.add(TCOP(text=track.copyright)) - if save['savePlaylistAsCompilation'] and "playlist" in track: + if save['savePlaylistAsCompilation'] and track.playlist: tag.add(TCMP(text="1")) if save['cover'] and track.album['picPath']: @@ -155,7 +155,7 @@ def tagFLAC(stream, track, save): if save['copyright']: tag["COPYRIGHT"] = track.copyright - if save['savePlaylistAsCompilation'] and "playlist" in track: + if save['savePlaylistAsCompilation'] and track.playlist: tag["COMPILATION"] = "1" if save['cover'] and track.album['picPath']: From 560fc700526c6e31a6aa89ed38ad63da3d204bd7 Mon Sep 17 00:00:00 2001 From: RemixDev Date: Sat, 15 Aug 2020 23:12:26 +0200 Subject: [PATCH 09/13] Fixed check for corrupted FLACs --- deemix/app/downloadjob.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/deemix/app/downloadjob.py b/deemix/app/downloadjob.py index 8a3adc8..4ccc10f 100644 --- a/deemix/app/downloadjob.py +++ b/deemix/app/downloadjob.py @@ -418,7 +418,7 @@ class DownloadJob: def downloadMusic(track, trackAPI_gw): try: with open(writepath, 'wb') as stream: - self.streamTrack(stream, track, trackAPI_gw) + self.streamTrack(stream, track) except DownloadCancelled: remove(writepath) raise DownloadCancelled @@ -476,7 +476,7 @@ class DownloadJob: 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) + self.removeTrackPercentage() track.filesizes['FILESIZE_FLAC'] = "0" return self.download(trackAPI_gw, track) if track.searched: @@ -528,14 +528,14 @@ class DownloadJob: return error_num # fallback is enabled and loop went through all formats - def streamTrack(self, stream, track, trackAPI): + def streamTrack(self, stream, track): if self.queueItem.cancel: raise DownloadCancelled try: request = get(track.downloadUrl, headers=self.dz.http_headers, stream=True, timeout=30) except ConnectionError: sleep(2) - return self.streamTrack(stream, track, trackAPI) + return self.streamTrack(stream, track) request.raise_for_status() blowfish_key = str.encode(self.dz._get_blowfish_key(str(track.id))) complete = int(request.headers["Content-Length"]) @@ -548,11 +548,11 @@ class DownloadJob: 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: + if isinstance(self.queueItem, QISingle): percentage = (chunkLength / complete) * 100 self.downloadPercentage = percentage else: - chunkProgres = (len(chunk) / complete) / trackAPI['SIZE'] * 100 + chunkProgres = (len(chunk) / complete) / self.queueItem.size * 100 self.downloadPercentage += chunkProgres self.updatePercentage() i += 1 From a3e9cdeca158570d8fa5b2be1b6708e8f8a705a4 Mon Sep 17 00:00:00 2001 From: RemixDev Date: Sat, 15 Aug 2020 23:36:32 +0200 Subject: [PATCH 10/13] Cleaned up afterDownload functions --- deemix/app/downloadjob.py | 208 ++++++++++++++++++++------------------ deemix/app/settings.py | 6 -- 2 files changed, 107 insertions(+), 107 deletions(-) diff --git a/deemix/app/downloadjob.py b/deemix/app/downloadjob.py index 4ccc10f..09d227c 100644 --- a/deemix/app/downloadjob.py +++ b/deemix/app/downloadjob.py @@ -49,83 +49,6 @@ errorMessages = { '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: @@ -177,25 +100,102 @@ class DownloadJob: self.downloadPercentage = 0 self.lastPercentage = 0 self.extrasPath = self.settings['downloadLocation'] + self.playlistPath = None + self.playlistURLs = [] def start(self): if isinstance(self.queueItem, QISingle): result = self.downloadWrapper(self.queueItem.single) if result: - download_path = after_download_single(result, self.settings) + singleAfterDownload(result) elif isinstance(self.queueItem, QICollection): - playlist = [None] * len(self.queueItem.collection) + tracks = [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) + tracks[pos] = executor.submit(self.downloadWrapper, track) + download_path = collectionAfterDownload(tracks) 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 + return self.extrasPath + + def singleAfterDownload(self, result): + # Save Album Cover + if self.settings['saveArtwork'] and 'albumPath' in result: + for image in result['albumURLs']: + downloadImage(image['url'], f"{result['albumPath']}.{image['ext']}", self.settings['overwriteFile']) + # Save Artist Artwork + if self.settings['saveArtworkArtist'] and 'artistPath' in result: + for image in result['artistURLs']: + downloadImage(image['url'], f"{result['artistPath']}.{image['ext']}", self.settings['overwriteFile']) + # Create searched logfile + if self.settings['logSearched'] and 'searched' in result: + with open(os.path.join(self.extrasPath, 'searched.txt'), 'wb+') as f: + orig = f.read().decode('utf-8') + if not result['searched'] in orig: + if orig != "": + orig += "\r\n" + orig += result['searched'] + "\r\n" + f.write(orig.encode('utf-8')) + # Execute command after download + if self.settings['executeCommand'] != "": + execute(self.settings['executeCommand'].replace("%folder%", self.extrasPath).replace("%filename%", result['filename'])) + + def collectionAfterDownload(self, tracks): + playlist = [None] * len(tracks) + errors = "" + searched = "" + + for index in range(len(tracks)): + result = tracks[index].result() + # Check if queue is cancelled + if not result: + return None + # Log errors to file + 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" + # Log searched to file + if 'searched' in result: + searched += result['searched'] + "\r\n" + # Save Album Cover + if self.settings['saveArtwork'] and 'albumPath' in result: + for image in result['albumURLs']: + downloadImage(image['url'], f"{result['albumPath']}.{image['ext']}", self.settings['overwriteFile']) + # Save Artist Artwork + if self.settings['saveArtworkArtist'] and 'artistPath' in result: + for image in result['artistURLs']: + downloadImage(image['url'], f"{result['artistPath']}.{image['ext']}", self.settings['overwriteFile']) + # Save filename for playlist file + playlist[index] = "" + if 'filename' in result: + playlist[index] = result['filename'] + + # Create errors logfile + if self.settings['logErrors'] and errors != "": + with open(os.path.join(self.extrasPath, 'errors.txt'), 'wb') as f: + f.write(errors.encode('utf-8')) + # Create searched logfile + if self.settings['logSearched'] and searched != "": + with open(os.path.join(self.extrasPath, 'searched.txt'), 'wb') as f: + f.write(searched.encode('utf-8')) + # Save Playlist Artwork + if self.settings['saveArtwork'] and self.playlistPath and not self.settings['tags']['savePlaylistAsCompilation']: + for image in self.playlistURLs: + downloadImage(image['url'], os.path.join(self.extrasPath, self.playlistPath)+f".{image['ext']}", self.settings['overwriteFile']) + # Create M3U8 File + if self.settings['createM3U8File']: + filename = settingsRegexPlaylistFile(self.settings['playlistFilenameTemplate'], queueItem, self.settings) or "playlist" + with open(os.path.join(self.extrasPath, filename+'.m3u8'), 'wb') as f: + for line in playlist: + f.write((line + "\n").encode('utf-8')) + # Execute command after download + if self.settings['executeCommand'] != "": + execute(self.settings['executeCommand'].replace("%folder%", self.extrasPath)) def download(self, trackAPI_gw, track=None): result = {} @@ -389,27 +389,28 @@ class DownloadJob: result['extrasPath'] = extrasPath # Data for m3u file - result['playlistPosition'] = writepath[len(extrasPath):] + result['filename'] = 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 len(self.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" + self.playlistURLs.append({'url': url, 'ext': format}) + else: + self.playlistURLs.append({'url': track.playlist['picUrl'], 'ext': 'jpg'}) + if not self.playlistPath: + 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']) + self.playlistPath = 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") @@ -422,7 +423,7 @@ class DownloadJob: except DownloadCancelled: remove(writepath) raise DownloadCancelled - except HTTPError: + except HTTPError, DownloadEmpty: remove(writepath) if track.fallbackId != "0": logger.warn(f"[{track.mainArtist['name']} - {track.title}] Track not available, using fallback id") @@ -539,6 +540,8 @@ class DownloadJob: request.raise_for_status() blowfish_key = str.encode(self.dz._get_blowfish_key(str(track.id))) complete = int(request.headers["Content-Length"]) + if complete == 0: + raise DownloadEmpty chunkLength = 0 percentage = 0 i = 0 @@ -629,3 +632,6 @@ class DownloadFailed(DownloadError): class DownloadCancelled(DownloadError): pass + +class DownloadEmpty(DownloadError): + pass diff --git a/deemix/app/settings.py b/deemix/app/settings.py index 52628b9..8a1d1b3 100644 --- a/deemix/app/settings.py +++ b/deemix/app/settings.py @@ -133,12 +133,6 @@ class Settings: for i in range(len(logslist)-5): remove(path.join(logspath, logslist[i])) - def getSettings(self): - return self.settings - - def getDefaultSettings(self): - return self.defaultSettings - # Saves the settings def saveSettings(self, newSettings=None): if newSettings: From 7088bff58d16753d60c582b244d0e52fee95cdad Mon Sep 17 00:00:00 2001 From: RemixDev Date: Sat, 15 Aug 2020 23:43:39 +0200 Subject: [PATCH 11/13] Fixed "Fixed check for corrupted FLACs" --- deemix/app/downloadjob.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deemix/app/downloadjob.py b/deemix/app/downloadjob.py index 09d227c..42facfd 100644 --- a/deemix/app/downloadjob.py +++ b/deemix/app/downloadjob.py @@ -423,7 +423,7 @@ class DownloadJob: except DownloadCancelled: remove(writepath) raise DownloadCancelled - except HTTPError, DownloadEmpty: + except (HTTPError, DownloadEmpty): remove(writepath) if track.fallbackId != "0": logger.warn(f"[{track.mainArtist['name']} - {track.title}] Track not available, using fallback id") From d4bcc728f49dc4e34329c7bbd36b339fdc0d190c Mon Sep 17 00:00:00 2001 From: RemixDev Date: Sun, 16 Aug 2020 10:41:18 +0200 Subject: [PATCH 12/13] Moved features from pyweb to the library Also fixed some errors --- deemix/app/downloadjob.py | 6 ++-- deemix/app/queueitem.py | 60 ++++++++++++++++++---------------- deemix/app/queuemanager.py | 66 ++++++++++++++++++++++++++++++++++---- 3 files changed, 95 insertions(+), 37 deletions(-) diff --git a/deemix/app/downloadjob.py b/deemix/app/downloadjob.py index 42facfd..80dbc25 100644 --- a/deemix/app/downloadjob.py +++ b/deemix/app/downloadjob.py @@ -107,13 +107,13 @@ class DownloadJob: if isinstance(self.queueItem, QISingle): result = self.downloadWrapper(self.queueItem.single) if result: - singleAfterDownload(result) + self.singleAfterDownload(result) elif isinstance(self.queueItem, QICollection): tracks = [None] * len(self.queueItem.collection) with ThreadPoolExecutor(self.settings['queueConcurrency']) as executor: for pos, track in enumerate(self.queueItem.collection, start=0): tracks[pos] = executor.submit(self.downloadWrapper, track) - download_path = collectionAfterDownload(tracks) + self.collectionAfterDownload(tracks) if self.interface: if self.queueItem.cancel: self.interface.send('currentItemCancelled', self.queueItem.uuid) @@ -486,7 +486,7 @@ class DownloadJob: 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}) + self.interface.send("updateQueue", {'uuid': self.queueItem.uuid, 'downloaded': True, 'downloadPath': writepath}) return result def getPreferredBitrate(self, track): diff --git a/deemix/app/queueitem.py b/deemix/app/queueitem.py index 7023715..582bef0 100644 --- a/deemix/app/queueitem.py +++ b/deemix/app/queueitem.py @@ -1,16 +1,22 @@ #!/usr/bin/env python3 class QueueItem: - def __init__(self, id=None, bitrate=None, title=None, artist=None, cover=None, size=None, type=None, settings=None, queueItemList=None): - if queueItemList: - self.title = queueItemList['title'] - self.artist = queueItemList['artist'] - self.cover = queueItemList['cover'] - self.size = queueItemList['size'] - self.type = queueItemList['type'] - self.id = queueItemList['id'] - self.bitrate = queueItemList['bitrate'] - self.settings = queueItemList['settings'] + def __init__(self, id=None, bitrate=None, title=None, artist=None, cover=None, size=None, type=None, settings=None, queueItemDict=None): + if queueItemDict: + self.title = queueItemDict['title'] + self.artist = queueItemDict['artist'] + self.cover = queueItemDict['cover'] + self.size = queueItemDict['size'] + self.type = queueItemDict['type'] + self.id = queueItemDict['id'] + self.bitrate = queueItemDict['bitrate'] + self.downloaded = queueItemDict['downloaded'] + self.failed = queueItemDict['failed'] + self.errors = queueItemDict['errors'] + self.progress = queueItemDict['progress'] + self.settings = None + if 'settings' in queueItemDict: + self.settings = queueItemDict['settings'] else: self.title = title self.artist = artist @@ -20,10 +26,10 @@ class QueueItem: self.id = id self.bitrate = bitrate self.settings = settings - self.downloaded = 0 - self.failed = 0 - self.errors = [] - self.progress = 0 + self.downloaded = 0 + self.failed = 0 + self.errors = [] + self.progress = 0 self.uuid = f"{self.type}_{self.id}_{self.bitrate}" self.cancel = False @@ -53,17 +59,17 @@ class QueueItem: def getSlimmedItem(self): light = self.toDict() - propertiesToDelete = ['single', 'collection', '_EXTRA'] + propertiesToDelete = ['single', 'collection', '_EXTRA', 'settings'] for property in propertiesToDelete: if property in light: del light[property] return light class QISingle(QueueItem): - def __init__(self, id=None, bitrate=None, title=None, artist=None, cover=None, type=None, settings=None, single=None, queueItemList=None): - if queueItemList: - super().__init__(queueItemList=queueItemList) - self.single = queueItemList['single'] + def __init__(self, id=None, bitrate=None, title=None, artist=None, cover=None, type=None, settings=None, single=None, queueItemDict=None): + if queueItemDict: + super().__init__(queueItemDict=queueItemDict) + self.single = queueItemDict['single'] else: super().__init__(id, bitrate, title, artist, cover, 1, type, settings) self.single = single @@ -74,10 +80,10 @@ class QISingle(QueueItem): return queueItem class QICollection(QueueItem): - def __init__(self, id=None, bitrate=None, title=None, artist=None, cover=None, size=None, type=None, settings=None, collection=None, queueItemList=None): - if queueItemList: - super().__init__(queueItemList=queueItemList) - self.collection = queueItemList['collection'] + def __init__(self, id=None, bitrate=None, title=None, artist=None, cover=None, size=None, type=None, settings=None, collection=None, queueItemDict=None): + if queueItemDict: + super().__init__(queueItemDict=queueItemDict) + self.collection = queueItemDict['collection'] else: super().__init__(id, bitrate, title, artist, cover, size, type, settings) self.collection = collection @@ -88,10 +94,10 @@ class QICollection(QueueItem): return queueItem class QIConvertable(QICollection): - def __init__(self, id=None, bitrate=None, title=None, artist=None, cover=None, size=None, type=None, settings=None, extra=None, queueItemList=None): - if queueItemList: - super().__init__(queueItemList=queueItemList) - self.extra = queueItemList['_EXTRA'] + def __init__(self, id=None, bitrate=None, title=None, artist=None, cover=None, size=None, type=None, settings=None, extra=None, queueItemDict=None): + if queueItemDict: + super().__init__(queueItemDict=queueItemDict) + self.extra = queueItemDict['_EXTRA'] else: super().__init__(id, bitrate, title, artist, cover, size, type, settings, []) self.extra = extra diff --git a/deemix/app/queuemanager.py b/deemix/app/queuemanager.py index d78e8db..2b48b64 100644 --- a/deemix/app/queuemanager.py +++ b/deemix/app/queuemanager.py @@ -5,7 +5,9 @@ from deemix.api.deezer import APIError from spotipy.exceptions import SpotifyException from deemix.app.queueitem import QISingle, QICollection import logging +import os.path as path import json +from os import remove logging.basicConfig(level=logging.INFO) logger = logging.getLogger('deemix') @@ -387,7 +389,7 @@ class QueueManager: if interface: interface.send("startDownload", self.currentItem) logger.info(f"[{self.currentItem}] Started downloading.") - DownloadJob(dz, sp, self.queueList[self.currentItem]).start() + DownloadJob(dz, sp, self.queueList[self.currentItem], interface).start() self.afterDownload(dz, sp, interface) def afterDownload(self, dz, sp, interface): @@ -401,14 +403,64 @@ class QueueManager: def getQueue(self): - return (self.queue, self.queueComplete, self.queueList, self.currentItem) + return (self.queue, self.queueComplete, self.slimQueueList(), self.currentItem) - # TODO: Convert dicts to QueueItem Objects when restoring - def restoreQueue(self, queue, queueComplete, queueList, dz, sp, interface): - self.queueComplete = queueComplete - self.queueList = queueList + def saveQueue(self, configFolder): + if len(self.queueList) > 0: + if self.currentItem != "": + self.queue.insert(0, self.currentItem) + with open(path.join(configFolder, 'queue.json'), 'w') as f: + json.dump({ + 'queue': self.queue, + 'queueComplete': self.queueComplete, + 'queueList': self.exportQueueList() + }, f) + + def exportQueueList(self): + queueList = {} + for uuid in self.queueList: + if uuid in self.queue: + queueList[uuid] = self.queueList[uuid].getResettedItem() + else: + queueList[uuid] = self.queueList[uuid].toDict() + print(self.queueList[uuid].progress) + return queueList + + def slimQueueList(self): + queueList = {} + for uuid in self.queueList: + queueList[uuid] = self.queueList[uuid].getSlimmedItem() + return queueList + + def loadQueue(self, dz, sp, configFolder, settings, interface=None): + if path.isfile(path.join(configFolder, 'queue.json')) and not len(self.queue): + if interface: + interface.send('restoringQueue') + with open(path.join(configFolder, 'queue.json'), 'r') as f: + qd = json.load(f) + remove(path.join(configFolder, 'queue.json')) + self.restoreQueue(qd['queue'], qd['queueComplete'], qd['queueList'], settings, dz, sp, interface) + if interface: + interface.send('init_downloadQueue', { + 'queue': self.queue, + 'queueComplete': self.queueComplete, + 'queueList': self.slimQueueList(), + 'restored': True + }) + self.nextItem(dz, sp, interface) + + def restoreQueue(self, queue, queueComplete, queueList, settings, dz, sp, interface=None): self.queue = queue - nextItem(dz, sp, interface) + self.queueComplete = queueComplete + self.queueList = {} + for uuid in queueList: + if 'single' in queueList[uuid]: + self.queueList[uuid] = QISingle(queueItemDict = queueList[uuid]) + if 'collection' in queueList[uuid]: + self.queueList[uuid] = QICollection(queueItemDict = queueList[uuid]) + if '_EXTRA' in queueList[uuid]: + self.queueList[uuid] = QIConvertable(queueItemDict = queueList[uuid]) + self.queueList[uuid].settings = settings def removeFromQueue(self, uuid, interface=None): if uuid == self.currentItem: From e8357cf5e193442a13e1cc65b1c175501332c5aa Mon Sep 17 00:00:00 2001 From: RemixDev Date: Sun, 16 Aug 2020 12:29:51 +0200 Subject: [PATCH 13/13] Fixed issues with spotify playlist and the queue manager --- deemix/__init__.py | 2 +- deemix/app/downloadjob.py | 4 ++-- deemix/app/queuemanager.py | 15 ++++++--------- deemix/app/spotifyhelper.py | 6 +++--- setup.py | 2 +- 5 files changed, 13 insertions(+), 16 deletions(-) diff --git a/deemix/__init__.py b/deemix/__init__.py index bfa5d4c..5c792be 100644 --- a/deemix/__init__.py +++ b/deemix/__init__.py @@ -1,3 +1,3 @@ #!/usr/bin/env python3 -__version__ = "1.1.31" +__version__ = "1.2.0" diff --git a/deemix/app/downloadjob.py b/deemix/app/downloadjob.py index 80dbc25..5dc4ffb 100644 --- a/deemix/app/downloadjob.py +++ b/deemix/app/downloadjob.py @@ -92,7 +92,7 @@ class DownloadJob: self.dz = dz self.sp = sp self.interface = interface - if isinstance(queueItem, QIConvertable): + if isinstance(queueItem, QIConvertable) and queueItem.extra: self.sp.convert_spotify_playlist(self.dz, queueItem, interface=self.interface) self.queueItem = queueItem self.settings = queueItem.settings @@ -609,7 +609,7 @@ class DownloadJob: if 'error' in result: self.completeTrackPercentage() self.queueItem.failed += 1 - self.queueItem.errors.append(result['error']['message']) + self.queueItem.errors.append(result['error']) if self.interface: error = result['error'] self.interface.send("updateQueue", { diff --git a/deemix/app/queuemanager.py b/deemix/app/queuemanager.py index 2b48b64..d185e1e 100644 --- a/deemix/app/queuemanager.py +++ b/deemix/app/queuemanager.py @@ -3,11 +3,12 @@ from deemix.app.downloadjob import DownloadJob from deemix.utils.misc import getIDFromLink, getTypeFromLink, getBitrateInt from deemix.api.deezer import APIError from spotipy.exceptions import SpotifyException -from deemix.app.queueitem import QISingle, QICollection +from deemix.app.queueitem import QISingle, QICollection, QIConvertable import logging import os.path as path import json from os import remove +from time import sleep logging.basicConfig(level=logging.INFO) logger = logging.getLogger('deemix') @@ -304,9 +305,7 @@ class QueueManager: return QueueError(url, "Spotify Features is not setted up correctly.", "spotifyDisabled") try: - playlist = sp.generate_playlist_queueitem(dz, id, settings) - playlist.bitrate = bitrate - return playlist + return sp.generate_playlist_queueitem(dz, id, bitrate, settings) except SpotifyException as e: return QueueError(url, "Wrong URL: "+e.msg[e.msg.find('\n')+2:]) @@ -423,7 +422,6 @@ class QueueManager: queueList[uuid] = self.queueList[uuid].getResettedItem() else: queueList[uuid] = self.queueList[uuid].toDict() - print(self.queueList[uuid].progress) return queueList def slimQueueList(self): @@ -432,14 +430,14 @@ class QueueManager: queueList[uuid] = self.queueList[uuid].getSlimmedItem() return queueList - def loadQueue(self, dz, sp, configFolder, settings, interface=None): + def loadQueue(self, configFolder, settings, interface=None): if path.isfile(path.join(configFolder, 'queue.json')) and not len(self.queue): if interface: interface.send('restoringQueue') with open(path.join(configFolder, 'queue.json'), 'r') as f: qd = json.load(f) remove(path.join(configFolder, 'queue.json')) - self.restoreQueue(qd['queue'], qd['queueComplete'], qd['queueList'], settings, dz, sp, interface) + self.restoreQueue(qd['queue'], qd['queueComplete'], qd['queueList'], settings) if interface: interface.send('init_downloadQueue', { 'queue': self.queue, @@ -447,9 +445,8 @@ class QueueManager: 'queueList': self.slimQueueList(), 'restored': True }) - self.nextItem(dz, sp, interface) - def restoreQueue(self, queue, queueComplete, queueList, settings, dz, sp, interface=None): + def restoreQueue(self, queue, queueComplete, queueList, settings): self.queue = queue self.queueComplete = queueComplete self.queueList = {} diff --git a/deemix/app/spotifyhelper.py b/deemix/app/spotifyhelper.py index ae7cafe..3f31eaf 100644 --- a/deemix/app/spotifyhelper.py +++ b/deemix/app/spotifyhelper.py @@ -183,7 +183,7 @@ class SpotifyHelper: return dz_album - def generate_playlist_queueitem(self, dz, playlist_id, settings): + def generate_playlist_queueitem(self, dz, playlist_id, bitrate, settings): if not self.spotifyEnabled: raise spotifyFeaturesNotEnabled spotify_playlist = self.sp.playlist(playlist_id) @@ -215,7 +215,7 @@ class SpotifyHelper: extra['playlistAPI'] = playlistAPI return QIConvertable( playlist_id, - 0, + bitrate, spotify_playlist['name'], spotify_playlist['owner']['display_name'], cover, @@ -264,7 +264,7 @@ class SpotifyHelper: collection.append(deezerTrack) convertPercentage = (pos / queueItem.size) * 100 - if round(convertPercentage) != lastPercentage and round(convertPercentage) % 2 == 0: + if round(convertPercentage) != lastPercentage and round(convertPercentage) % 5 == 0: lastPercentage = round(convertPercentage) if interface: interface.send("updateQueue", {'uuid': queueItem.uuid, 'conversion': lastPercentage}) diff --git a/setup.py b/setup.py index 5ccb94b..f555f57 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ README = (HERE / "README.md").read_text() setup( name="deemix", - version="1.1.31", + version="1.2.0", description="A barebone deezer downloader library", long_description=README, long_description_content_type="text/markdown",