diff --git a/deemix/app/default.json b/deemix/app/default.json index 3518b46..2c59d4d 100644 --- a/deemix/app/default.json +++ b/deemix/app/default.json @@ -1,18 +1,17 @@ { "downloadLocation": "", "tracknameTemplate": "%artist% - %title%", - "albumTracknameTemplate": "%number% - %title%", + "albumTracknameTemplate": "%tracknumber% - %title%", "playlistTracknameTemplate": "%position% - %artist% - %title%", "createPlaylistFolder": true, - "playlistNameTemplate": "%name%", + "playlistNameTemplate": "%playlist%", "createArtistFolder": false, - "artistNameTemplate": "%name%", + "artistNameTemplate": "%artist%", "createAlbumFolder": true, "albumNameTemplate": "%artist% - %album%", "createCDFolder": true, "createStructurePlaylist": false, "createSingleFolder": false, - "saveFullArtists": false, "padTracks": true, "paddingSize": "0", "illegalCharacterReplacer": "_", diff --git a/deemix/app/downloader.py b/deemix/app/downloader.py index a14313c..4898c27 100644 --- a/deemix/app/downloader.py +++ b/deemix/app/downloader.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 from deemix.api.deezer import Deezer, APIError from deemix.utils.taggers import tagID3, tagFLAC +from deemix.utils.pathtemplates import generateFilename, generateFilepath import os.path +from os import makedirs from urllib.error import HTTPError dz = Deezer() @@ -16,6 +18,38 @@ extensions = { 13: '.mp4' } +def getPreferredBitrare(filesize, bitrate): + bitrateFound = False; + selectedFormat = 0 + selectedFilesize = 0 + if int(bitrate) == 9: + selectedFormat = 9 + selectedFilesize = filesize['flac'] + if filesize['flac'] > 0: + bitrateFound = True + else: + bitrateFound = False + bitrate = 3 + if int(bitrate) == 3: + selectedFormat = 3 + selectedFilesize = filesize['mp3_320'] + if filesize['mp3_320'] > 0: + bitrateFound = True + else: + bitrateFound = False + bitrate = 1 + if int(bitrate) == 1: + selectedFormat = 3 + selectedFilesize = filesize['mp3_320'] + if filesize['mp3_320'] > 0: + bitrateFound = True + else: + bitrateFound = False + if not bitrateFound: + selectedFormat = 8 + selectedFilesize = filesize['default'] + return (selectedFormat, selectedFilesize) + def parseEssentialTrackData(track, trackAPI): track['id'] = trackAPI['SNG_ID'] track['duration'] = trackAPI['DURATION'] @@ -57,7 +91,10 @@ def getTrackData(trackAPI): track['mainArtist'] = {} track['mainArtist']['id'] = 0 track['mainArtist']['name'] = trackAPI['ART_NAME'] - track['artistArray'] = [trackAPI['ART_NAME']] + track['artists'] = [trackAPI['ART_NAME']] + track['aritst'] = { + 'Main': [trackAPI['ART_NAME']] + } track['date'] = { 'day': 0, 'month': 0, @@ -72,10 +109,12 @@ def getTrackData(trackAPI): track['explicit'] = trackAPI['EXPLICIT_LYRICS'] != "0" if 'COPYRIGHT' in trackAPI: track['copyright'] = trackAPI['COPYRIGHT'] - track['replayGain'] = "{0:.2f} dB".format((float(trackAPI['GAIN']) + 18.4) * -1) + track['replayGain'] = "{0:.2f} dB".format((float(trackAPI['GAIN']) + 18.4) * -1) if 'GAIN' in trackAPI else None track['ISRC'] = trackAPI['ISRC'] track['trackNumber'] = trackAPI['TRACK_NUMBER'] track['contributors'] = trackAPI['SNG_CONTRIBUTORS'] + if 'POSITION' in trackAPI: + track['position'] = trackAPI['POSITION'] track['lyrics'] = {} if 'LYRICS_ID' in trackAPI: @@ -130,8 +169,8 @@ def getTrackData(trackAPI): track['album']['label'] = albumAPI['label'] if 'label' in albumAPI else "Unknown" if not 'pic' in track['album']: track['album']['pic'] = albumAPI['cover_small'][43:-24] - if 'release_date' in albumAPI and not 'date' in track: - track['date'] = { + 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] @@ -155,18 +194,21 @@ def getTrackData(trackAPI): track['album']['label'] = albumAPI2['LABEL_NAME'] if 'LABEL_NAME' in albumAPI2 else "Unknown" if not 'pic' in track['album']: track['album']['pic'] = albumAPI2['ALB_PICTURE'] - if 'PHYSICAL_RELEASE_DATE' in albumAPI2 and not 'date' in track: - track['date'] = { + if 'PHYSICAL_RELEASE_DATE' in albumAPI2: + track['album']['date'] = { 'day': albumAPI2["PHYSICAL_RELEASE_DATE"][8:10], 'month': albumAPI2["PHYSICAL_RELEASE_DATE"][5:7], 'year': albumAPI2["PHYSICAL_RELEASE_DATE"][0:4] } track['album']['genre'] = [] + if 'date' in track['album']: + track['date'] = track['album']['date'] + trackAPI2 = dz.get_track(track['id']) track['bpm'] = trackAPI2['bpm'] if not 'replayGain' in track: - track['replayGain'] = "{0:.2f} dB".format((float(trackAPI2['gain']) + 18.4) * -1) + track['replayGain'] = "{0:.2f} dB".format((float(trackAPI2['gain']) + 18.4) * -1) if 'GAIN' in trackAPI else "" if not 'explicit' in track: track['explicit'] = trackAPI2['explicit_lyrics'] if not 'discNumber' in track: @@ -204,43 +246,19 @@ def downloadTrackObj(trackAPI, settings, overwriteBitrate=False, extraTrack=None bitrate = overwriteBitrate else: bitrate = settings['maxBitrate'] - bitrateFound = False; - if int(bitrate) == 9: - track['selectedFormat'] = 9 - track['selectedFilesize'] = track['filesize']['flac'] - if track['filesize']['flac'] > 0: - bitrateFound = True - else: - bitrateFound = False - bitrate = 3 - if int(bitrate) == 3: - track['selectedFormat'] = 3 - track['selectedFilesize'] = track['filesize']['mp3_320'] - if track['filesize']['mp3_320'] > 0: - bitrateFound = True - else: - bitrateFound = False - bitrate = 1 - if int(bitrate) == 1: - track['selectedFormat'] = 3 - track['selectedFilesize'] = track['filesize']['mp3_320'] - if track['filesize']['mp3_320'] > 0: - bitrateFound = True - else: - bitrateFound = False - if not bitrateFound: - track['selectedFormat'] = 8 - track['selectedFilesize'] = track['filesize']['default'] - track['album']['bitrate'] = track['selectedFormat'] + (format, filesize) = getPreferredBitrare(track['filesize'], bitrate) + track['selectedFormat'] = format + track['selectedFilesize'] = filesize + track['album']['bitrate'] = format track['album']['picUrl'] = "http://e-cdn-images.deezer.com/images/cover/{}/{}x{}-000000-80-0-0.jpg".format(track['album']['pic'], settings['embeddedArtworkSize'], settings['embeddedArtworkSize']) - # Create the filename - filename = "{artist} - {title}".format(title=track['title'], artist=track['mainArtist']['name']) + extensions[ - track['selectedFormat']] - writepath = os.path.join(settings['downloadLocation'], filename) + filename = generateFilename(track, trackAPI, settings) + extensions[track['selectedFormat']] + (filepath, artistPath, coverPath, extrasPath) = generateFilepath(track, trackAPI, settings) - track['downloadUrl'] = dz.get_track_stream_url(track['id'], track['MD5'], track['mediaVersion'], - track['selectedFormat']) + makedirs(filepath, exist_ok=True) + writepath = os.path.join(filepath, filename) + + track['downloadUrl'] = dz.get_track_stream_url(track['id'], track['MD5'], track['mediaVersion'], track['selectedFormat']) with open(writepath, 'wb') as stream: try: dz.stream_track(track['id'], track['downloadUrl'], stream) @@ -268,6 +286,8 @@ def downloadTrackObj(trackAPI, settings, overwriteBitrate=False, extraTrack=None def download_track(id, settings, overwriteBitrate=False): trackAPI = dz.get_track_gw(id) + trackAPI['FILENAME_TEMPLATE'] = settings['tracknameTemplate'] + trackAPI['SINGLE_TRACK'] = True downloadTrackObj(trackAPI, settings, overwriteBitrate) def download_album(id, settings, overwriteBitrate=False): @@ -278,16 +298,21 @@ def download_album(id, settings, overwriteBitrate=False): if albumAPI['nb_tracks'] == 1: trackAPI = dz.get_track_gw(albumAPI['tracks']['data'][0]['id']) trackAPI['ALBUM_EXTRA'] = albumAPI + trackAPI['FILENAME_TEMPLATE'] = settings['tracknameTemplate'] + trackAPI['SINGLE_TRACK'] = True downloadTrackObj(trackAPI, settings, overwriteBitrate) else: tracksArray = dz.get_album_tracks_gw(id) for trackAPI in tracksArray: trackAPI['ALBUM_EXTRA'] = albumAPI + trackAPI['FILENAME_TEMPLATE'] = settings['albumTracknameTemplate'] downloadTrackObj(trackAPI, settings, overwriteBitrate) def download_playlist(id, settings, overwriteBitrate=False): playlistAPI = dz.get_playlist(id) playlistTracksAPI = dz.get_playlist_tracks_gw(id) - for trackAPI in playlistTracksAPI: + for pos, trackAPI in enumerate(playlistTracksAPI, start=1): trackAPI['PLAYLIST_EXTRA'] = playlistAPI + trackAPI['POSITION'] = pos + trackAPI['FILENAME_TEMPLATE'] = settings['playlistTracknameTemplate'] downloadTrackObj(trackAPI, settings, overwriteBitrate) diff --git a/deemix/utils/pathtemplates.py b/deemix/utils/pathtemplates.py new file mode 100644 index 0000000..97ac8db --- /dev/null +++ b/deemix/utils/pathtemplates.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +import re +from os.path import sep as pathSep + +bitrateLabels = { + 15: "360 HQ", + 14: "360 MQ", + 13: "360 LQ", + 9: "FLAC", + 3: "320", + 1: "128", + 8: "128" +} + +def fixName(txt, char='_'): + txt = str(txt) + txt = re.sub(r'[\0\/\\:*?"<>|]', char, txt) + return txt + +def fixLongName(name): + if pathSep in name: + name2 = name.split(pathSep) + name = "" + for txt in name: + txt = txt[:200] + name += txt+pathSep + name = name[:-1] + else: + name = name[:200] + return name + +def antiDot(str): + while str[-1:] == "." or str[-1:] == " " or str[-1:] == "\n": + str = str[:-1] + if len(str) < 1: + str = "dot" + return str + +def pad(num, max, dopad=True): + paddingsize = len(str(max)) + if dopad: + return str(num).zfill(paddingsize) + else: + return str(num) + +def generateFilename(track, trackAPI, settings): + if trackAPI['FILENAME_TEMPLATE'] == "": + filename = "%artist% - %title%" + else: + filename = trackAPI['FILENAME_TEMPLATE'] + return settingsRegex(filename, track, settings, trackAPI['PLAYLIST_EXTRA'] if 'PLAYLIST_EXTRA' in trackAPI else None) + +def generateFilepath(track, trackAPI, settings): + filepath = settings['downloadLocation'] + if filepath[-1:] != pathSep: + filepath += pathSep + artistPath = None + coverPath = None + extrasPath = None + + if settings['createPlaylistFolder'] and 'PLAYLIST_EXTRA' in trackAPI and not settings['savePlaylistAsCompilation']: + filepath += antiDot(settingsRegexPlaylist(settings['playlistNameTemplate'], trackAPI['PLAYLIST_EXTRA'], settings)) + pathSep + + if 'PLAYLIST_EXTRA' in trackAPI and not settings['savePlaylistAsCompilation']: + extrasPath = filepath + + if ( + settings['createArtistFolder'] and not 'PLAYLIST_EXTRA' in trackAPI or + (settings['createArtistFolder'] and 'PLAYLIST_EXTRA' in trackAPI and settings['savePlaylistAsCompilation']) or + (settings['createArtistFolder'] and 'PLAYLIST_EXTRA' in trackAPI and settings['createStructurePlaylist']) + ): + if (track['id']<0 and not 'artist' in track['album']): + track['album']['artist'] = track['mainArtist'] + filepath += antiDot(settingsRegexArtist(settings['artistNameTemplate'], track['album']['artist'], settings)) + pathSep + artistPath = filepath + + if (settings['createAlbumFolder'] and + (not 'SINGLE_TRACK' in trackAPI or ('SINGLE_TRACK' in trackAPI and settings['createSingleFolder'])) and + (not 'PLAYLIST_EXTRA' in trackAPI or ('PLAYLIST_EXTRA' in trackAPI and settings['savePlaylistAsCompilation']) or ('PLAYLIST_EXTRA' in trackAPI and settings['createStructurePlaylist'])) + ): + filepath += antiDot(settingsRegexAlbum(settings['albumNameTemplate'], track['album'], settings)) + pathSep + coverPath = filepath + + if not ('PLAYLIST_EXTRA' in trackAPI and not settings['savePlaylistAsCompilation']): + extrasPath = filepath + + if ( + 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 'PLAYLIST_EXTRA' in trackAPI or ('PLAYLIST_EXTRA' in trackAPI and settings['savePlaylistAsCompilation']) or ('PLAYLIST_EXTRA' in trackAPI and settings['createStructurePlaylist'])) + )): + 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("%album%", fixName(track['album']['title'], settings['illegalCharacterReplacer'])) + filename = filename.replace("%albumartist%", fixName(track['album']['artist']['name'], settings['illegalCharacterReplacer'])) + filename = filename.replace("%tracknumber%", pad(track['trackNumber'], track['album']['trackTotal'], 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("%genre%", 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%", "{}-{}-{}".format(str(track['date']['year']), str(track['date']['month']), str(track['date']['day']))) + 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'])) + if playlist: + filename = filename.replace("%playlist_id%", str(playlist['id'])) + filename = filename.replace("%position%", pad(track['position'], playlist['nb_tracks'], settings['padTracks'])) + else: + filename = filename.replace("%position%", pad(track['trackNumber'], track['album']['trackTotal'], settings['padTracks'])) + filename = re.sub(r'[/\\]', pathSep, filename) + return antiDot(fixLongName(filename)) + +def settingsRegexAlbum(foldername, album, settings): + foldername = foldername.replace("%album_id%", str(album['id'])) + foldername = foldername.replace("%album%", fixName(album['title'], settings['illegalCharacterReplacer'])) + foldername = foldername.replace("%artist%", fixName(album['artist']['name'], settings['illegalCharacterReplacer'])) + foldername = foldername.replace("%artist_id%", str(album['artist']['id'])) + foldername = foldername.replace("%tracktotal%", str(album['trackTotal'])) + foldername = foldername.replace("%disctotal%", str(album['discTotal'])) + foldername = foldername.replace("%type%", fixName(album['recordType'], settings['illegalCharacterReplacer'])) + foldername = foldername.replace("%upc%", album['barcode']) + foldername = foldername.replace("%label%", fixName(album['label'], settings['illegalCharacterReplacer'])) + if len(album['genre'])>0: + foldername = foldername.replace("%genre%", fixName(album['genre'][0], settings['illegalCharacterReplacer'])) + else: + foldername = foldername.replace("%genre%", "Unknown") + foldername = foldername.replace("%year%", str(album['date']['year'])) + foldername = foldername.replace("%date%", "{}-{}-{}".format(str(album['date']['year']), str(album['date']['month']), str(album['date']['day']))) + foldername = foldername.replace("%bitrate%", bitrateLabels[int(album['bitrate'])]) + + foldername = re.sub(r'[/\\]', pathSep, foldername) + return antiDot(fixLongName(foldername)) + +def settingsRegexArtist(foldername, artist, settings): + foldername = foldername.replace("%artist%", fixName(artist['name'], settings['illegalCharacterReplacer'])) + foldername = foldername.replace("%artist_id%", str(artist['id'])) + foldername = re.sub(r'[/\\]', pathSep, foldername) + return antiDot(fixLongName(foldername)) + +def settingsRegexPlaylist(foldername, playlist, settings): + foldername = foldername.replace("%playlist%", fixName(playlist['title'], settings['illegalCharacterReplacer'])) + foldername = foldername.replace("%playlist_id%", fixName(playlist['id'], settings['illegalCharacterReplacer'])) + foldername = foldername.replace("%owner%", fixName(playlist['creator']['name'], settings['illegalCharacterReplacer'])) + foldername = foldername.replace("%owner_id%", str(playlist['creator']['id'])) + foldername = foldername.replace("%year%", str(playlist['creation_date'][:4])) + foldername = foldername.replace("%date%", str(playlist['creation_date'][:10])) + foldername = re.sub(r'[/\\]', pathSep, foldername) + return antiDot(fixLongName(foldername))