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/__main__.py b/deemix/__main__.py index ae5ce1b..ed2533b 100644 --- a/deemix/__main__.py +++ b/deemix/__main__.py @@ -1,30 +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 - @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(local) + 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 - -def main(): - download() + click.echo(app.set.settings['downloadLocation']) #folder name output if __name__ == '__main__': - main() + download() diff --git a/deemix/api/deezer.py b/deemix/api/deezer.py index e8c25aa..e0e09de 100755 --- a/deemix/api/deezer.py +++ b/deemix/api/deezer.py @@ -257,7 +257,35 @@ 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'] + 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 = [] @@ -449,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 9d389f1..d126811 100644 --- a/deemix/app/__init__.py +++ b/deemix/app/__init__.py @@ -1,2 +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.spotifyhelper import SpotifyHelper + +class deemix: + def __init__(self, configFolder=None): + self.set = Settings(configFolder) + self.dz = Deezer() + 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/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/downloader.py b/deemix/app/downloader.py deleted file mode 100644 index 4f0e034..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 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 'cancel' in queueItem: - 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 'cancel' in queueItem: - 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'] - - return track - - -def downloadTrackObj(dz, trackAPI, settings, bitrate, queueItem, extraTrack=None, interface=None): - result = {} - if 'cancel' in queueItem: - 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 'cancel' in queueItem: - 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 "_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" - 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 'cancel' in queueItem: - 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, queueItem, interface=None): - global downloadPercentage, lastPercentage - settings = queueItem['settings'] - bitrate = queueItem['bitrate'] - downloadPercentage = 0 - lastPercentage = 0 - if 'single' in queueItem: - 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, queueItem) - elif 'collection' in queueItem: - 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 'cancel' in queueItem: - interface.send('currentItemCancelled', queueItem['uuid']) - interface.send("removedFromQueue", queueItem['uuid']) - else: - interface.send("finishDownload", queueItem['uuid']) - return { - 'dz': dz, - 'interface': interface, - 'download_path': 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, queueItem): - 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/app/downloadjob.py b/deemix/app/downloadjob.py new file mode 100644 index 0000000..5dc4ffb --- /dev/null +++ b/deemix/app/downloadjob.py @@ -0,0 +1,637 @@ +#!/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 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.interface = interface + 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 + self.bitrate = queueItem.bitrate + 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: + 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) + self.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 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 = {} + 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 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"[{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) + 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"[{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) + 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']}_{self.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['picPath'] = os.path.join(TEMPDIR, f"alb{track.album['id']}_{self.settings['embeddedArtworkSize']}.jpg") + track.album['bitrate'] = selectedFormat + + 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 + # 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 + 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['filename'] = writepath[len(extrasPath):] + + # Save playlist cover + if track.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") + track.downloadUrl = self.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) + except DownloadCancelled: + remove(writepath) + raise DownloadCancelled + except (HTTPError, DownloadEmpty): + 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"[{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) + 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 e + 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") + self.completeTrackPercentage() + + # 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() + track.filesizes['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': self.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 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) + 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 + 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 isinstance(self.queueItem, QISingle): + percentage = (chunkLength / complete) * 100 + self.downloadPercentage = percentage + else: + chunkProgres = (len(chunk) / complete) / self.queueItem.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': 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: + result = self.download(trackAPI_gw) + except DownloadCancelled: + return None + except DownloadFailed as error: + 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(f"[{track['artist']} - {track['title']}] {str(e)}") + result = {'error': { + 'message': str(e), + 'data': track + }} + + if 'error' in result: + self.completeTrackPercentage() + self.queueItem.failed += 1 + self.queueItem.errors.append(result['error']) + if self.interface: + error = result['error'] + self.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 + +class DownloadEmpty(DownloadError): + pass 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 new file mode 100644 index 0000000..582bef0 --- /dev/null +++ b/deemix/app/queueitem.py @@ -0,0 +1,108 @@ +#!/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, 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 + 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}" + 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, + 'errors': self.errors, + 'progress': self.progress, + 'type': self.type, + 'id': self.id, + 'bitrate': self.bitrate, + 'uuid': self.uuid + } + + 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', '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, 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 + + 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, 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 + + def toDict(self): + queueItem = super().toDict() + queueItem['collection'] = self.collection + 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, 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 + + 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 9fde329..d185e1e 100644 --- a/deemix/app/queuemanager.py +++ b/deemix/app/queuemanager.py @@ -1,533 +1,511 @@ #!/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 +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') -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() - if 'single' in light: - del light['single'] - if 'collection' in light: - del light['collection'] - return light - -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"): + 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: + 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 ''}") 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 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) + 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) + if not playlistAPI['public'] and playlistAPI['creator']['id'] != str(dz.user['id']): + logger.warn("You can't download others private playlists.") + 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 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 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 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 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 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: + 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:]) - 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 - if interface: - interface.send("startConvertingSpotifyPlaylist", str(id)) - try: - playlist = sp.convert_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" - result['errid'] = "unsupportedURL" - return result + logger.warn("URL not supported yet") + return QueueError(url, "URL not supported yet", "unsupportedURL") + def addToQueue(self, dz, sp, url, settings, bitrate=None, interface=None): + if not dz.logged_in: + if interface: + interface.send("loginNeededToDownload") + return False -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 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, interface) - return True + return self.generateQueueItem(dz, sp, link, settings, bitrate, interface=interface) - -def nextItem(dz, interface=None): - global currentItem, queueList, queue - if currentItem != "": - return None - else: - if len(queue) > 0: - currentItem = queue.pop(0) + 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.") + DownloadJob(dz, sp, self.queueList[self.currentItem], interface).start() + 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.slimQueueList(), self.currentItem) + + 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() + return queueList + + def slimQueueList(self): + queueList = {} + for uuid in self.queueList: + queueList[uuid] = self.queueList[uuid].getSlimmedItem() + return queueList + + 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) + if interface: + interface.send('init_downloadQueue', { + 'queue': self.queue, + 'queueComplete': self.queueComplete, + 'queueList': self.slimQueueList(), + 'restored': True + }) + + def restoreQueue(self, queue, queueComplete, queueList, settings): + self.queue = queue + 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: + 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, 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['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") +class QueueError: + def __init__(self, link, message, errid=None): + self.link = link + self.message = message + self.errid = errid -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") + def toDict(self): + return { + 'link': self.link, + 'error': self.message, + 'errid': self.errid + } diff --git a/deemix/app/settings.py b/deemix/app/settings.py index eff73e5..8a1d1b3 100644 --- a/deemix/app/settings.py +++ b/deemix/app/settings.py @@ -3,8 +3,6 @@ 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 @@ -14,93 +12,151 @@ logger = logging.getLogger('deemix') import deemix.utils.localpaths as localpaths -settings = {} -defaultSettings = {} -configDir = "" +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 + } + } -def initSettings(localFolder = False, 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() + # Create config folder if it doesn't exsist + makedirs(self.configFolder, exist_ok=True) - if localFolder: - settings['downloadLocation'] = randomString(12) - logger.info("Using a local download folder: "+settings['downloadLocation']) - elif settings['downloadLocation'] == "": - settings['downloadLocation'] = path.join(localpaths.getHomeFolder(), 'deemix Music') - saveSettings(settings) - makedirs(settings['downloadLocation'], 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) - # 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])) + # Read config file + with open(path.join(self.configFolder, 'config.json'), 'r') as configFile: + self.settings = json.load(configFile) - return settings + self.settingsCheck() + # Make sure the download path exsits + makedirs(self.settings['downloadLocation'], exist_ok=True) -def getSettings(): - global settings - return settings + # 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) -def getDefaultSettings(): - global defaultSettings - return defaultSettings + # 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 saveSettings(newSettings): - global settings - settings = newSettings - with open(path.join(configDir, 'config.json'), 'w') as configFile: - json.dump(settings, configFile, indent=2) - return True + # 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) - -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] + # 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 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)) + 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/spotify.py b/deemix/app/spotifyhelper.py similarity index 74% rename from deemix/app/spotify.py rename to deemix/app/spotifyhelper.py index 5542d74..3f31eaf 100644 --- a/deemix/app/spotify.py +++ b/deemix/app/spotifyhelper.py @@ -6,42 +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, 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() @@ -51,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: @@ -62,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'] @@ -114,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 @@ -147,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 @@ -174,50 +182,65 @@ class SpotifyHelper: json.dump(cache, spotifyCache) return dz_album - def convert_spotify_playlist(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) - 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'] = [] - tracklist = [] 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) - result['size'] = totalSize + if item['track']['explicit']: + playlistAPI['explicit'] = True + extra['unconverted'].append(item['track']) + + totalSize = len(extra['unconverted']) + if not 'explicit' in playlistAPI: + playlistAPI['explicit'] = False + extra['playlistAPI'] = playlistAPI + return QIConvertable( + playlist_id, + bitrate, + spotify_playlist['name'], + spotify_playlist['owner']['display_name'], + cover, + totalSize, + 'spotify_playlist', + settings, + extra, + ) + + def convert_spotify_playlist(self, dz, queueItem, 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", queueItem.uuid) + collection = [] + 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 = { @@ -234,18 +257,25 @@ 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'] = queueItem.extra['playlistAPI'] deezerTrack['POSITION'] = pos - deezerTrack['SIZE'] = totalSize - deezerTrack['FILENAME_TEMPLATE'] = settings['playlistTracknameTemplate'] - result['collection'].append(deezerTrack) - if not 'explicit' in playlistAPI: - playlistAPI['explicit'] = False + deezerTrack['SIZE'] = queueItem.size + deezerTrack['FILENAME_TEMPLATE'] = queueItem.settings['playlistTracknameTemplate'] + collection.append(deezerTrack) + + convertPercentage = (pos / queueItem.size) * 100 + if round(convertPercentage) != lastPercentage and round(convertPercentage) % 5 == 0: + lastPercentage = round(convertPercentage) + if interface: + interface.send("updateQueue", {'uuid': queueItem.uuid, 'conversion': lastPercentage}) + + queueItem.extra = None + queueItem.collection = collection + with open(path.join(self.configFolder, 'spotifyCache.json'), 'w') as spotifyCache: json.dump(cache, spotifyCache) - return result + if interface: + 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 new file mode 100644 index 0000000..c9826d6 --- /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']: + self.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.filesizes = dz.get_track_filesizes(self.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'] + + self.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" + + self.mainArtist = { + 'id': trackAPI_gw['ART_ID'], + 'name': trackAPI_gw['ART_NAME'], + 'pic': None + } + if 'ART_PICTURE' in trackAPI_gw: + self.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 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: + 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/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..2c57a4a 100644 --- a/deemix/utils/misc.py +++ b/deemix/utils/misc.py @@ -34,6 +34,30 @@ 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 + 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('?')] @@ -94,12 +118,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/pathtemplates.py b/deemix/utils/pathtemplates.py index 7b21ef6..aef5ef7 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(", ".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 track.featArtistsString 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)) @@ -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)) diff --git a/deemix/utils/taggers.py b/deemix/utils/taggers.py index 1cfa420..8c4fc96 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() @@ -12,141 +13,156 @@ 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'])) - if save['albumArtist'] and len(track['album']['artists']): - if save['singleAlbumArtist'] and track['album']['mainArtist']['save']: - tag.add(TPE2(text=track['album']['mainArtist']['name'])) + 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'])) + 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'])) - if save['savePlaylistAsCompilation'] and "playlist" in track: + tag.add(TCOP(text=track.copyright)) + if save['savePlaylistAsCompilation'] and track.playlist: 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 '/') - +# 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']): + tag["TITLE"] = track.title + + 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'] - if save['albumArtist'] and len(track['album']['artists']): + tag["ALBUM"] = track.album['title'] + + 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'] - for role in track['contributors']: + 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] + 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'] - if save['savePlaylistAsCompilation'] and "playlist" in track: + tag["COPYRIGHT"] = track.copyright + if save['savePlaylistAsCompilation'] and track.playlist: 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) 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",