diff --git a/deemix/downloader.py b/deemix/downloader.py index 4f5ac74..5286c48 100644 --- a/deemix/downloader.py +++ b/deemix/downloader.py @@ -20,6 +20,7 @@ from mutagen.flac import FLACNoHeaderError, error as FLACError from deezer import TrackFormats from deezer.errors import WrongLicense, WrongGeolocation +from deezer.utils import map_track from deemix.types.DownloadObjects import Single, Collection from deemix.types.Track import Track from deemix.types.Picture import StaticPicture @@ -74,7 +75,7 @@ def downloadImage(url, path, overwrite=OverwriteOption.DONT_OVERWRITE): pictureSize = int(pictureUrl[:pictureUrl.find("x")]) if pictureSize > 1200: return downloadImage(urlBase+pictureUrl.replace(f"{pictureSize}x{pictureSize}", '1200x1200'), path, overwrite) - except (requests.exceptions.ConnectionError, requests.exceptions.ChunkedEncodingError, u3SSLError) as e: + except (requests.exceptions.ConnectionError, requests.exceptions.ChunkedEncodingError, u3SSLError): if path.is_file(): path.unlink() sleep(5) return downloadImage(url, path, overwrite) @@ -84,7 +85,7 @@ def downloadImage(url, path, overwrite=OverwriteOption.DONT_OVERWRITE): logger.exception("Error while downloading an image, you should report this to the developers: %s", e) return None -def getPreferredBitrate(dz, track, preferredBitrate, shouldFallback, uuid=None, listener=None): +def getPreferredBitrate(dz, track, preferredBitrate, shouldFallback, feelingLucky, uuid=None, listener=None): preferredBitrate = int(preferredBitrate) falledBack = False @@ -101,32 +102,31 @@ def getPreferredBitrate(dz, track, preferredBitrate, shouldFallback, uuid=None, ) try: request.raise_for_status() - track.filesizes[f"FILESIZE_{formatName}"] = int(request.headers["Content-Length"]) - track.filesizes[f"FILESIZE_{formatName}_TESTED"] = True - return track.filesizes[f"FILESIZE_{formatName}"] != 0 + track.filesizes[f"{formatName.lower()}"] = int(request.headers["Content-Length"]) + track.filesizes[f"{formatName.lower()}_TESTED"] = True + return track.filesizes[f"{formatName.lower()}"] != 0 except requests.exceptions.HTTPError: # if the format is not available, Deezer returns a 403 error return False - def getCorrectURL(track, formatName, formatNumber): + def getCorrectURL(track, formatName, formatNumber, feelingLucky): nonlocal wrongLicense, isGeolocked url = None # Check the track with the legit method - try: - url = dz.get_track_url(track.trackToken, formatName) - if testURL(track, url, formatName): return url - url = None - except (WrongLicense, WrongGeolocation) as e: - wrongLicense = isinstance(e, WrongLicense) - isGeolocked = isinstance(e, WrongGeolocation) + if formatName.lower() in track.filesizes and track.filesizes[formatName.lower()] != "0": + try: + url = dz.get_track_url(track.trackToken, formatName) + except (WrongLicense, WrongGeolocation) as e: + wrongLicense = isinstance(e, WrongLicense) + isGeolocked = isinstance(e, WrongGeolocation) # Fallback to old method - if not url: + if not url and feelingLucky: url = generateCryptedStreamURL(track.id, track.MD5, track.mediaVersion, formatNumber) if testURL(track, url, formatName): return url url = None return url if track.local: - url = getCorrectURL(track, "MP3_MISC", TrackFormats.LOCAL) + url = getCorrectURL(track, "MP3_MISC", TrackFormats.LOCAL, feelingLucky) track.urls["MP3_MISC"] = url return TrackFormats.LOCAL @@ -150,20 +150,23 @@ def getPreferredBitrate(dz, track, preferredBitrate, shouldFallback, uuid=None, else: formats = formats_non_360 + # check and renew trackToken before starting the check + track.checkAndRenewTrackToken(dz) for formatNumber, formatName in formats.items(): # Current bitrate is higher than preferred bitrate; skip if formatNumber > preferredBitrate: continue currentTrack = track - url = getCorrectURL(currentTrack, formatName, formatNumber) + url = getCorrectURL(currentTrack, formatName, formatNumber, feelingLucky) newTrack = None while True: if not url and hasAlternative: newTrack = dz.gw.get_track_with_fallback(currentTrack.fallbackID) + newTrack = map_track(newTrack) currentTrack = Track() currentTrack.parseEssentialData(newTrack) hasAlternative = currentTrack.fallbackID != "0" - if not url: getCorrectURL(currentTrack, formatName, formatNumber) + if not url: getCorrectURL(currentTrack, formatName, formatNumber, feelingLucky) if (url or not hasAlternative): break if url: @@ -189,7 +192,7 @@ def getPreferredBitrate(dz, track, preferredBitrate, shouldFallback, uuid=None, }, }) if is360format: raise TrackNot360 - url = getCorrectURL(track, "MP3_MISC", TrackFormats.DEFAULT) + url = getCorrectURL(track, "MP3_MISC", TrackFormats.DEFAULT, feelingLucky) track.urls["MP3_MISC"] = url return TrackFormats.DEFAULT @@ -208,17 +211,16 @@ class Downloader: if not self.downloadObject.isCanceled: if isinstance(self.downloadObject, Single): track = self.downloadWrapper({ - 'trackAPI_gw': self.downloadObject.single['trackAPI_gw'], 'trackAPI': self.downloadObject.single.get('trackAPI'), 'albumAPI': self.downloadObject.single.get('albumAPI') }) if track: self.afterDownloadSingle(track) elif isinstance(self.downloadObject, Collection): - tracks = [None] * len(self.downloadObject.collection['tracks_gw']) + tracks = [None] * len(self.downloadObject.collection['tracks']) with ThreadPoolExecutor(self.settings['queueConcurrency']) as executor: - for pos, track in enumerate(self.downloadObject.collection['tracks_gw'], start=0): + for pos, track in enumerate(self.downloadObject.collection['tracks'], start=0): tracks[pos] = executor.submit(self.downloadWrapper, { - 'trackAPI_gw': track, + 'trackAPI': track, 'albumAPI': self.downloadObject.collection.get('albumAPI'), 'playlistAPI': self.downloadObject.collection.get('playlistAPI') }) @@ -241,17 +243,17 @@ class Downloader: def download(self, extraData, track=None): returnData = {} - trackAPI_gw = extraData['trackAPI_gw'] trackAPI = extraData.get('trackAPI') albumAPI = extraData.get('albumAPI') playlistAPI = extraData.get('playlistAPI') + trackAPI['size'] = self.downloadObject.size if self.downloadObject.isCanceled: raise DownloadCanceled - if trackAPI_gw['SNG_ID'] == "0": raise DownloadFailed("notOnDeezer") + if int(trackAPI['id']) == 0: raise DownloadFailed("notOnDeezer") itemData = { - 'id': trackAPI_gw['SNG_ID'], - 'title': trackAPI_gw['SNG_TITLE'].strip(), - 'artist': trackAPI_gw['ART_NAME'] + 'id': trackAPI['id'], + 'title': trackAPI['title'], + 'artist': trackAPI['artist']['name'] } # Create Track object @@ -260,7 +262,7 @@ class Downloader: try: track = Track().parseData( dz=self.dz, - trackAPI_gw=trackAPI_gw, + track_id=trackAPI['id'], trackAPI=trackAPI, albumAPI=albumAPI, playlistAPI=playlistAPI @@ -287,13 +289,13 @@ class Downloader: self.dz, track, self.bitrate, - self.settings['fallbackBitrate'], + self.settings['fallbackBitrate'], self.settings['feelingLucky'], self.downloadObject.uuid, self.listener ) except WrongLicense as e: raise DownloadFailed("wrongLicense") from e except WrongGeolocation as e: - raise DownloadFailed("wrongGeolocation") from e + raise DownloadFailed("wrongGeolocation", track) from e except PreferredBitrateNotFound as e: raise DownloadFailed("wrongBitrate", track) from e except TrackNot360 as e: @@ -432,7 +434,7 @@ class Downloader: self.downloadObject.removeTrackProgress(self.listener) track.filesizes['FILESIZE_FLAC'] = "0" track.filesizes['FILESIZE_FLAC_TESTED'] = True - return self.download(trackAPI_gw, track=track) + return self.download(extraData, track=track) self.log(itemData, "tagged") if track.searched: returnData['searched'] = True @@ -450,18 +452,13 @@ class Downloader: return returnData def downloadWrapper(self, extraData, track=None): - trackAPI_gw = extraData['trackAPI_gw'] - if trackAPI_gw.get('_EXTRA_TRACK'): - extraData['trackAPI'] = trackAPI_gw['_EXTRA_TRACK'].copy() - del extraData['trackAPI_gw']['_EXTRA_TRACK'] + trackAPI = extraData['trackAPI'] # Temp metadata to generate logs itemData = { - 'id': trackAPI_gw['SNG_ID'], - 'title': trackAPI_gw['SNG_TITLE'].strip(), - 'artist': trackAPI_gw['ART_NAME'] + 'id': trackAPI['id'], + 'title': trackAPI['title'], + 'artist': trackAPI['artist']['name'] } - if trackAPI_gw.get('VERSION') and trackAPI_gw['VERSION'] not in trackAPI_gw['SNG_TITLE']: - itemData['title'] += f" {trackAPI_gw['VERSION']}".strip() try: result = self.download(extraData, track) @@ -471,16 +468,30 @@ class Downloader: if track.fallbackID != "0": self.warn(itemData, error.errid, 'fallback') newTrack = self.dz.gw.get_track_with_fallback(track.fallbackID) + newTrack = map_track(newTrack) track.parseEssentialData(newTrack) - track.retriveFilesizes(self.dz) return self.downloadWrapper(extraData, track) + if len(track.albumsFallback) != 0 and self.settings.fallbackISRC: + newAlbumID = track.albumsFallback.pop() + newAlbum = self.dz.gw.get_album_page(newAlbumID) + fallbackID = 0 + for newTrack in newAlbum['SONGS']['data']: + if newTrack['ISRC'] == track.ISRC: + fallbackID = newTrack['SNG_ID'] + break + if fallbackID != 0: + self.warn(itemData, error.errid, 'fallback') + newTrack = self.dz.gw.get_track_with_fallback(fallbackID) + newTrack = map_track(newTrack) + track.parseEssentialData(newTrack) + return self.downloadWrapper(extraData, track) if not track.searched and self.settings['fallbackSearch']: self.warn(itemData, error.errid, 'search') searchedId = self.dz.api.get_track_id_from_metadata(track.mainArtist.name, track.title, track.album.title) if searchedId != "0": newTrack = self.dz.gw.get_track_with_fallback(searchedId) + newTrack = map_track(newTrack) track.parseEssentialData(newTrack) - track.retriveFilesizes(self.dz) track.searched = True self.log(itemData, "searchFallback") return self.downloadWrapper(extraData, track) diff --git a/deemix/errors.py b/deemix/errors.py index 62001ce..c4b6f6d 100644 --- a/deemix/errors.py +++ b/deemix/errors.py @@ -60,7 +60,8 @@ ErrorMessages = { 'noSpaceLeft': "No space left on target drive, clean up some space for the tracks", 'albumDoesntExists': "Track's album does not exsist, failed to gather info.", 'notLoggedIn': "You need to login to download tracks.", - 'wrongGeolocation': "Your account can't stream the track from your current country." + 'wrongGeolocation': "Your account can't stream the track from your current country.", + 'wrongGeolocationNoAlternative': "Your account can't stream the track from your current country and no alternative found." } class DownloadFailed(DownloadError): diff --git a/deemix/itemgen.py b/deemix/itemgen.py index 8366d76..ec0881c 100644 --- a/deemix/itemgen.py +++ b/deemix/itemgen.py @@ -1,8 +1,7 @@ import logging -from deezer.gw import LyricsStatus from deezer.errors import GWAPIError, APIError -from deezer.utils import map_user_playlist +from deezer.utils import map_user_playlist, map_track, map_album from deemix.types.DownloadObjects import Single, Collection from deemix.errors import GenerationError, ISRCnotOnDeezer, InvalidID, NotYourPrivatePlaylist @@ -10,40 +9,44 @@ from deemix.errors import GenerationError, ISRCnotOnDeezer, InvalidID, NotYourPr logger = logging.getLogger('deemix') def generateTrackItem(dz, link_id, bitrate, trackAPI=None, albumAPI=None): - # Check if is an isrc: url - if str(link_id).startswith("isrc"): - try: - trackAPI = dz.api.get_track(link_id) - except APIError as e: - raise GenerationError(f"https://deezer.com/track/{link_id}", str(e)) from e + # Get essential track info + if not trackAPI: + if str(link_id).startswith("isrc") or int(link_id) > 0: + try: + trackAPI = dz.api.get_track(link_id) + except APIError as e: + raise GenerationError(f"https://deezer.com/track/{link_id}", str(e)) from e - if 'id' in trackAPI and 'title' in trackAPI: - link_id = trackAPI['id'] + # Check if is an isrc: url + if str(link_id).startswith("isrc"): + if 'id' in trackAPI and 'title' in trackAPI: + link_id = trackAPI['id'] + else: + raise ISRCnotOnDeezer(f"https://deezer.com/track/{link_id}") else: - raise ISRCnotOnDeezer(f"https://deezer.com/track/{link_id}") + trackAPI_gw = dz.gw.get_track(link_id) + trackAPI = map_track(trackAPI_gw) + else: + link_id = trackAPI['id'] if not str(link_id).strip('-').isdecimal(): raise InvalidID(f"https://deezer.com/track/{link_id}") - # Get essential track info - try: - trackAPI_gw = dz.gw.get_track_with_fallback(link_id) - except GWAPIError as e: - raise GenerationError(f"https://deezer.com/track/{link_id}", str(e)) from e + cover = None + if trackAPI['album']['cover_small']: + cover = trackAPI['album']['cover_small'][:-24] + '/75x75-000000-80-0-0.jpg' + else: + cover = f"https://e-cdns-images.dzcdn.net/images/cover/{trackAPI['md5_image']}/75x75-000000-80-0-0.jpg" - title = trackAPI_gw['SNG_TITLE'].strip() - if trackAPI_gw.get('VERSION') and trackAPI_gw['VERSION'] not in trackAPI_gw['SNG_TITLE']: - title += f" {trackAPI_gw['VERSION']}".strip() - explicit = bool(int(trackAPI_gw.get('EXPLICIT_LYRICS', 0))) + del trackAPI['track_token'] return Single({ 'type': 'track', 'id': link_id, 'bitrate': bitrate, - 'title': title, - 'artist': trackAPI_gw['ART_NAME'], - 'cover': f"https://e-cdns-images.dzcdn.net/images/cover/{trackAPI_gw['ALB_PICTURE']}/75x75-000000-80-0-0.jpg", - 'explicit': explicit, + 'title': trackAPI['title'], + 'artist': trackAPI['artist']['name'], + 'cover': cover, + 'explicit': trackAPI.explicit_lyrics, 'single': { - 'trackAPI_gw': trackAPI_gw, 'trackAPI': trackAPI, 'albumAPI': albumAPI } @@ -66,7 +69,14 @@ def generateAlbumItem(dz, link_id, bitrate, rootArtist=None): link_id = albumAPI['id'] else: try: - albumAPI = dz.api.get_album(link_id) + albumAPI_gw_page = dz.gw.get_album_page(link_id) + if 'DATA' in albumAPI_gw_page: + albumAPI = map_album(albumAPI_gw_page['DATA']) + link_id = albumAPI_gw_page['DATA']['ALB_ID'] + albumAPI_new = dz.api.get_album(link_id) + albumAPI.update(albumAPI_new) + else: + raise GenerationError(f"https://deezer.com/album/{link_id}", "Can't find the album") except APIError as e: raise GenerationError(f"https://deezer.com/album/{link_id}", str(e)) from e @@ -75,9 +85,9 @@ def generateAlbumItem(dz, link_id, bitrate, rootArtist=None): # Get extra info about album # This saves extra api calls when downloading albumAPI_gw = dz.gw.get_album(link_id) - albumAPI['nb_disk'] = albumAPI_gw['NUMBER_DISK'] - albumAPI['copyright'] = albumAPI_gw['COPYRIGHT'] - albumAPI['release_date'] = albumAPI_gw['PHYSICAL_RELEASE_DATE'] + albumAPI_gw = map_album(albumAPI_gw) + albumAPI_gw.update(albumAPI) + albumAPI = albumAPI_gw albumAPI['root_artist'] = rootArtist # If the album is a single download as a track @@ -91,18 +101,17 @@ def generateAlbumItem(dz, link_id, bitrate, rootArtist=None): if albumAPI['cover_small'] is not 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" + cover = f"https://e-cdns-images.dzcdn.net/images/cover/{albumAPI['md5_image']}/75x75-000000-80-0-0.jpg" totalSize = len(tracksArray) albumAPI['nb_tracks'] = totalSize collection = [] for pos, trackAPI in enumerate(tracksArray, start=1): - trackAPI['POSITION'] = pos - trackAPI['SIZE'] = totalSize + trackAPI = map_track(trackAPI) + del trackAPI['track_token'] + trackAPI['position'] = pos collection.append(trackAPI) - explicit = albumAPI_gw.get('EXPLICIT_ALBUM_CONTENT', {}).get('EXPLICIT_LYRICS_STATUS', LyricsStatus.UNKNOWN) in [LyricsStatus.EXPLICIT, LyricsStatus.PARTIALLY_EXPLICIT] - return Collection({ 'type': 'album', 'id': link_id, @@ -110,10 +119,10 @@ def generateAlbumItem(dz, link_id, bitrate, rootArtist=None): 'title': albumAPI['title'], 'artist': albumAPI['artist']['name'], 'cover': cover, - 'explicit': explicit, + 'explicit': albumAPI['explicit_lyrics'], 'size': totalSize, 'collection': { - 'tracks_gw': collection, + 'tracks': collection, 'albumAPI': albumAPI } }) @@ -147,10 +156,11 @@ def generatePlaylistItem(dz, link_id, bitrate, playlistAPI=None, playlistTracksA playlistAPI['nb_tracks'] = totalSize collection = [] for pos, trackAPI in enumerate(playlistTracksAPI, start=1): - if trackAPI.get('EXPLICIT_TRACK_CONTENT', {}).get('EXPLICIT_LYRICS_STATUS', LyricsStatus.UNKNOWN) in [LyricsStatus.EXPLICIT, LyricsStatus.PARTIALLY_EXPLICIT]: + trackAPI = map_track(trackAPI) + if trackAPI['explicit_lyrics']: playlistAPI['explicit'] = True - trackAPI['POSITION'] = pos - trackAPI['SIZE'] = totalSize + del trackAPI['track_token'] + trackAPI['position'] = pos collection.append(trackAPI) if 'explicit' not in playlistAPI: playlistAPI['explicit'] = False @@ -165,7 +175,7 @@ def generatePlaylistItem(dz, link_id, bitrate, playlistAPI=None, playlistTracksA 'explicit': playlistAPI['explicit'], 'size': totalSize, 'collection': { - 'tracks_gw': collection, + 'tracks': collection, 'playlistAPI': playlistAPI } }) diff --git a/deemix/plugins/spotify.py b/deemix/plugins/spotify.py index f553927..63bfde7 100644 --- a/deemix/plugins/spotify.py +++ b/deemix/plugins/spotify.py @@ -143,7 +143,7 @@ class Spotify(Plugin): 'explicit': playlistAPI['explicit'], 'size': len(tracklist), 'collection': { - 'tracks_gw': [], + 'tracks': [], 'playlistAPI': playlistAPI }, 'plugin': 'spotify', @@ -217,31 +217,31 @@ class Spotify(Plugin): if cachedTrack.get('id', "0") != "0": trackAPI = dz.api.get_track(cachedTrack['id']) - deezerTrack = None if not trackAPI: - deezerTrack = { - 'SNG_ID': "0", - 'SNG_TITLE': track['name'], - 'DURATION': 0, - 'MD5_ORIGIN': 0, - 'MEDIA_VERSION': 0, - 'FILESIZE': 0, - 'ALB_TITLE': track['album']['name'], - 'ALB_PICTURE': "", - 'ART_ID': 0, - 'ART_NAME': track['artists'][0]['name'] + trackAPI = { + 'id': "0", + 'title': track['name'], + 'duration': 0, + 'md5_origin': 0, + 'media_version': 0, + 'filesizes': {}, + 'album': { + 'title': track['album']['name'], + 'md5_image': "" + }, + 'artist': { + 'id': 0, + 'name': track['artists'][0]['name'] + } } - else: - deezerTrack = dz.gw.get_track_with_fallback(trackAPI['id']) - deezerTrack['_EXTRA_TRACK'] = trackAPI - deezerTrack['POSITION'] = pos+1 + trackAPI['position'] = pos+1 conversion['next'] += (1 / downloadObject.size) * 100 if round(conversion['next']) != conversion['now'] and round(conversion['next']) % 2 == 0: conversion['now'] = round(conversion['next']) if listener: listener.send("updateQueue", {'uuid': downloadObject.uuid, 'conversion': conversion['now']}) - return deezerTrack + return trackAPI def convert(self, dz, downloadObject, settings, listener=None): cache = self.loadCache() @@ -259,7 +259,7 @@ class Spotify(Plugin): cache, listener ).result() - downloadObject.collection['tracks_gw'] = collection + downloadObject.collection['tracks'] = collection downloadObject.size = len(collection) downloadObject = Collection(downloadObject.toDict()) if listener: listener.send("finishConversion", downloadObject.getSlimmedDict()) diff --git a/deemix/settings.py b/deemix/settings.py index 1f0656c..9cb0e7c 100644 --- a/deemix/settings.py +++ b/deemix/settings.py @@ -39,8 +39,10 @@ DEFAULTS = { "illegalCharacterReplacer": "_", "queueConcurrency": 3, "maxBitrate": str(TrackFormats.MP3_320), + "feelingLucky": False, "fallbackBitrate": False, "fallbackSearch": False, + "fallbackISRC": False, "logErrors": True, "logSearched": False, "overwriteFile": OverwriteOption.DONT_OVERWRITE, diff --git a/deemix/types/Album.py b/deemix/types/Album.py index 7522e27..3edf40f 100644 --- a/deemix/types/Album.py +++ b/deemix/types/Album.py @@ -1,5 +1,3 @@ -from deezer.gw import LyricsStatus - from deemix.utils import removeDuplicateArtists, removeFeatures from deemix.types.Artist import Artist from deemix.types.Date import Date @@ -30,7 +28,7 @@ class Album: self.rootArtist = None self.variousArtists = None - self.playlistId = None + self.playlistID = None self.owner = None self.isPlaylist = False @@ -39,8 +37,9 @@ class Album: # Getting artist image ID # ex: https://e-cdns-images.dzcdn.net/images/artist/f2bc007e9133c946ac3c3907ddc5d2ea/56x56-000000-80-0-0.jpg - art_pic = albumAPI['artist']['picture_small'] - art_pic = art_pic[art_pic.find('artist/') + 7:-24] + art_pic = albumAPI['artist'].get('picture_small') + if art_pic: art_pic = art_pic[art_pic.find('artist/') + 7:-24] + else: art_pic = "" self.mainArtist = Artist( albumAPI['artist']['id'], albumAPI['artist']['name'], @@ -83,52 +82,31 @@ class Album: self.barcode = albumAPI.get('upc', self.barcode) self.label = albumAPI.get('label', self.label) self.explicit = bool(albumAPI.get('explicit_lyrics', False)) - if 'release_date' in albumAPI: - self.date.day = albumAPI["release_date"][8:10] - self.date.month = albumAPI["release_date"][5:7] - self.date.year = albumAPI["release_date"][0:4] + release_date = albumAPI.get('release_date') + if 'physical_release_date' in albumAPI: + release_date = albumAPI['physical_release_date'] + if release_date: + self.date.day = release_date[8:10] + self.date.month = release_date[5:7] + self.date.year = release_date[0:4] self.date.fixDayMonth() self.discTotal = albumAPI.get('nb_disk') self.copyright = albumAPI.get('copyright') - if self.pic.md5 == "" and albumAPI.get('cover_small'): - # Getting album cover MD5 - # ex: https://e-cdns-images.dzcdn.net/images/cover/2e018122cb56986277102d2041a592c8/56x56-000000-80-0-0.jpg - alb_pic = albumAPI['cover_small'] - self.pic.md5 = alb_pic[alb_pic.find('cover/') + 6:-24] + if self.pic.md5 == "": + if albumAPI.get('md5_image'): + self.pic.md5 = albumAPI['md5_image'] + elif albumAPI.get('cover_small'): + # Getting album cover MD5 + # ex: https://e-cdns-images.dzcdn.net/images/cover/2e018122cb56986277102d2041a592c8/56x56-000000-80-0-0.jpg + alb_pic = albumAPI['cover_small'] + self.pic.md5 = alb_pic[alb_pic.find('cover/') + 6:-24] if albumAPI.get('genres') and len(albumAPI['genres'].get('data', [])) > 0: for genre in albumAPI['genres']['data']: self.genre.append(genre['name']) - def parseAlbumGW(self, albumAPI_gw): - self.title = albumAPI_gw['ALB_TITLE'] - self.mainArtist = Artist( - art_id = albumAPI_gw['ART_ID'], - name = albumAPI_gw['ART_NAME'], - role = "Main" - ) - - self.artists = [albumAPI_gw['ART_NAME']] - self.trackTotal = albumAPI_gw['NUMBER_TRACK'] - self.discTotal = albumAPI_gw['NUMBER_DISK'] - self.label = albumAPI_gw.get('LABEL_NAME', self.label) - - explicitLyricsStatus = albumAPI_gw.get('EXPLICIT_ALBUM_CONTENT', {}).get('EXPLICIT_LYRICS_STATUS', LyricsStatus.UNKNOWN) - self.explicit = explicitLyricsStatus in [LyricsStatus.EXPLICIT, LyricsStatus.PARTIALLY_EXPLICIT] - - self.addExtraAlbumGWData(albumAPI_gw) - - def addExtraAlbumGWData(self, albumAPI_gw): - if self.pic.md5 == "" and albumAPI_gw.get('ALB_PICTURE'): - self.pic.md5 = albumAPI_gw['ALB_PICTURE'] - if 'PHYSICAL_RELEASE_DATE' in albumAPI_gw: - self.date.day = albumAPI_gw["PHYSICAL_RELEASE_DATE"][8:10] - self.date.month = albumAPI_gw["PHYSICAL_RELEASE_DATE"][5:7] - self.date.year = albumAPI_gw["PHYSICAL_RELEASE_DATE"][0:4] - self.date.fixDayMonth() - def makePlaylistCompilation(self, playlist): self.variousArtists = playlist.variousArtists self.mainArtist = playlist.mainArtist diff --git a/deemix/types/Track.py b/deemix/types/Track.py index 7716a78..40bf065 100644 --- a/deemix/types/Track.py +++ b/deemix/types/Track.py @@ -1,9 +1,9 @@ -from time import sleep import re -import requests +from datetime import datetime +from deezer.utils import map_track, map_album from deezer.errors import APIError, GWAPIError -from deemix.errors import TrackError, NoDataToParse, AlbumDoesntExists +from deemix.errors import NoDataToParse, AlbumDoesntExists from deemix.utils import removeFeatures, andCommaConcat, removeDuplicateArtists, generateReplayGainString, changeCase @@ -24,8 +24,10 @@ class Track: self.MD5 = "" self.mediaVersion = "" self.trackToken = "" + self.trackTokenExpiration = 0 self.duration = 0 self.fallbackID = "0" + self.albumsFallback = [] self.filesizes = {} self.local = False self.mainArtist = None @@ -42,6 +44,7 @@ class Track: self.explicit = False self.ISRC = "" self.replayGain = "" + self.rank = 0 self.playlist = None self.position = None self.searched = False @@ -53,79 +56,54 @@ class Track: self.featArtistsString = "" self.urls = {} - def parseEssentialData(self, trackAPI_gw, trackAPI=None): - self.id = str(trackAPI_gw['SNG_ID']) - self.duration = trackAPI_gw['DURATION'] - self.trackToken = trackAPI_gw['TRACK_TOKEN'] - self.MD5 = trackAPI_gw.get('MD5_ORIGIN') - if not self.MD5: - if trackAPI and trackAPI.get('md5_origin'): - self.MD5 = trackAPI['md5_origin'] - #else: - # raise MD5NotFound - self.mediaVersion = trackAPI_gw['MEDIA_VERSION'] + def parseEssentialData(self, trackAPI): + self.id = str(trackAPI['id']) + self.duration = trackAPI['duration'] + self.trackToken = trackAPI['track_token'] + self.trackTokenExpiration = trackAPI['track_token_expire'] + self.MD5 = trackAPI.get('md5_origin') + self.mediaVersion = trackAPI['media_version'] self.fallbackID = "0" - if 'FALLBACK' in trackAPI_gw: - self.fallbackID = trackAPI_gw['FALLBACK']['SNG_ID'] + if 'fallback_id' in trackAPI: + self.fallbackID = trackAPI['fallback_id'] self.local = int(self.id) < 0 self.urls = {} - def retriveFilesizes(self, dz): - guest_sid = dz.session.cookies.get('sid') - try: - site = requests.post( - "https://api.deezer.com/1.0/gateway.php", - params={ - 'api_key': "4VCYIJUCDLOUELGD1V8WBVYBNVDYOXEWSLLZDONGBBDFVXTZJRXPR29JRLQFO6ZE", - 'sid': guest_sid, - 'input': '3', - 'output': '3', - 'method': 'song_getData' - }, - timeout=30, - json={'sng_id': self.id}, - headers=dz.http_headers - ) - result_json = site.json() - except: - sleep(2) - self.retriveFilesizes(dz) - if len(result_json['error']): - raise TrackError(result_json.dumps(result_json['error'])) - response = result_json.get("results", {}) - filesizes = {} - for key, value in response.items(): - if key.startswith("FILESIZE_"): - filesizes[key] = int(value) - filesizes[key+"_TESTED"] = False - self.filesizes = filesizes + def parseData(self, dz, track_id=None, trackAPI=None, albumAPI=None, playlistAPI=None): + if track_id and (not trackAPI or trackAPI and not trackAPI.get('track_token')): + trackAPI_new = dz.gw.get_track_with_fallback(track_id) + trackAPI_new = map_track(trackAPI_new) + if not trackAPI: trackAPI = {} + trackAPI_new.update(trackAPI) + trackAPI = trackAPI_new + elif not trackAPI: raise NoDataToParse - def parseData(self, dz, track_id=None, trackAPI_gw=None, trackAPI=None, albumAPI_gw=None, albumAPI=None, playlistAPI=None): - if track_id and not trackAPI_gw: trackAPI_gw = dz.gw.get_track_with_fallback(track_id) - elif not trackAPI_gw: raise NoDataToParse - if not trackAPI: - try: trackAPI = dz.api.get_track(trackAPI_gw['SNG_ID']) - except APIError: trackAPI = None + self.parseEssentialData(trackAPI) - self.parseEssentialData(trackAPI_gw, trackAPI) + # only public api has bpm + if not trackAPI.get('bpm') and not self.local: + try: + trackAPI_new = dz.api.get_track(trackAPI['id']) + trackAPI_new['release_date'] = trackAPI['release_date'] + trackAPI.update(trackAPI_new) + except APIError: pass if self.local: - self.parseLocalTrackData(trackAPI_gw) + self.parseLocalTrackData(trackAPI) else: - self.retriveFilesizes(dz) - self.parseTrackGW(trackAPI_gw) + self.parseTrack(trackAPI) # Get Lyrics data - if not "LYRICS" in trackAPI_gw and self.lyrics.id != "0": - try: trackAPI_gw["LYRICS"] = dz.gw.get_track_lyrics(self.id) + if not trackAPI.get("lyrics") and self.lyrics.id != "0": + try: trackAPI["lyrics"] = dz.gw.get_track_lyrics(self.id) except GWAPIError: self.lyrics.id = "0" - if self.lyrics.id != "0": self.lyrics.parseLyrics(trackAPI_gw["LYRICS"]) + if self.lyrics.id != "0": self.lyrics.parseLyrics(trackAPI["lyrics"]) # Parse Album Data self.album = Album( - alb_id = trackAPI_gw['ALB_ID'], - title = trackAPI_gw['ALB_TITLE'], - pic_md5 = trackAPI_gw.get('ALB_PICTURE') + alb_id = trackAPI['album']['id'], + title = trackAPI['album']['title'], + pic_md5 = trackAPI['album'].get('md5_origin') ) # Get album Data @@ -134,31 +112,31 @@ class Track: except APIError: albumAPI = None # Get album_gw Data - if not albumAPI_gw: - try: albumAPI_gw = dz.gw.get_album(self.album.id) - except GWAPIError: albumAPI_gw = None + # Only gw has disk number + if not albumAPI or albumAPI and not albumAPI.get('nb_disk'): + try: + albumAPI_gw = dz.gw.get_album(self.album.id) + albumAPI_gw = map_album(albumAPI_gw) + except GWAPIError: albumAPI_gw = {} + if not albumAPI: albumAPI = {} + albumAPI_gw.update(albumAPI) + albumAPI = albumAPI_gw - if albumAPI: - self.album.parseAlbum(albumAPI) - elif albumAPI_gw: - self.album.parseAlbumGW(albumAPI_gw) - # albumAPI_gw doesn't contain the artist cover - # Getting artist image ID - # ex: https://e-cdns-images.dzcdn.net/images/artist/f2bc007e9133c946ac3c3907ddc5d2ea/56x56-000000-80-0-0.jpg + if not albumAPI: raise AlbumDoesntExists + + self.album.parseAlbum(albumAPI) + # albumAPI_gw doesn't contain the artist cover + # Getting artist image ID + # ex: https://e-cdns-images.dzcdn.net/images/artist/f2bc007e9133c946ac3c3907ddc5d2ea/56x56-000000-80-0-0.jpg + if not self.album.mainArtist.pic.md5 or self.album.mainArtist.pic.md5 == "": artistAPI = dz.api.get_artist(self.album.mainArtist.id) self.album.mainArtist.pic.md5 = artistAPI['picture_small'][artistAPI['picture_small'].find('artist/') + 7:-24] - else: - raise AlbumDoesntExists # Fill missing data - if albumAPI_gw: self.album.addExtraAlbumGWData(albumAPI_gw) if self.album.date and not self.date: self.date = self.album.date - if not self.album.discTotal: self.album.discTotal = albumAPI_gw.get('NUMBER_DISK', "1") - if not self.copyright: self.copyright = albumAPI_gw['COPYRIGHT'] - if 'GENRES' in trackAPI_gw: - for genre in trackAPI_gw['GENRES']: + if 'genres' in trackAPI: + for genre in trackAPI['genres']: if genre not in self.album.genre: self.album.genre.append(genre) - self.parseTrack(trackAPI) # Remove unwanted charaters in track name # Example: track/127793 @@ -168,7 +146,7 @@ class Track: if len(self.artist['Main']) == 0: self.artist['Main'] = [self.mainArtist.name] - self.position = trackAPI_gw.get('POSITION') + self.position = trackAPI.get('position') # Add playlist data if track is in a playlist if playlistAPI: self.playlist = Playlist(playlistAPI) @@ -176,63 +154,52 @@ class Track: self.generateMainFeatStrings() return self - def parseLocalTrackData(self, trackAPI_gw): + def parseLocalTrackData(self, trackAPI): # Local tracks has only the trackAPI_gw page and # contains only the tags provided by the file - self.title = trackAPI_gw['SNG_TITLE'] - self.album = Album(title=trackAPI_gw['ALB_TITLE']) + self.title = trackAPI['title'] + self.album = Album(title=trackAPI['album']['title']) self.album.pic = Picture( - md5 = trackAPI_gw.get('ALB_PICTURE', ""), + md5 = trackAPI.get('md5_image', ""), pic_type = "cover" ) - self.mainArtist = Artist(name=trackAPI_gw['ART_NAME'], role="Main") - self.artists = [trackAPI_gw['ART_NAME']] + self.mainArtist = Artist(name=trackAPI['artist']['name'], role="Main") + self.artists = [trackAPI['artist']['name']] self.artist = { - 'Main': [trackAPI_gw['ART_NAME']] + 'Main': [trackAPI['artist']['name']] } self.album.artist = self.artist self.album.artists = self.artists self.album.date = self.date self.album.mainArtist = self.mainArtist - def parseTrackGW(self, trackAPI_gw): - self.title = trackAPI_gw['SNG_TITLE'].strip() - if trackAPI_gw.get('VERSION') and not trackAPI_gw['VERSION'].strip() in self.title: - self.title += f" {trackAPI_gw['VERSION'].strip()}" - - self.discNumber = trackAPI_gw.get('DISK_NUMBER') - self.explicit = bool(int(trackAPI_gw.get('EXPLICIT_LYRICS', "0"))) - self.copyright = trackAPI_gw.get('COPYRIGHT') - if 'GAIN' in trackAPI_gw: self.replayGain = generateReplayGainString(trackAPI_gw['GAIN']) - self.ISRC = trackAPI_gw.get('ISRC') - self.trackNumber = trackAPI_gw['TRACK_NUMBER'] - self.contributors = trackAPI_gw['SNG_CONTRIBUTORS'] - self.rank = trackAPI_gw['RANK_SNG'] - - self.lyrics = Lyrics(trackAPI_gw.get('LYRICS_ID', "0")) - - self.mainArtist = Artist( - art_id = trackAPI_gw['ART_ID'], - name = trackAPI_gw['ART_NAME'], - role = "Main", - pic_md5 = trackAPI_gw.get('ART_PICTURE') - ) - - if 'PHYSICAL_RELEASE_DATE' in trackAPI_gw: - self.date.day = trackAPI_gw["PHYSICAL_RELEASE_DATE"][8:10] - self.date.month = trackAPI_gw["PHYSICAL_RELEASE_DATE"][5:7] - self.date.year = trackAPI_gw["PHYSICAL_RELEASE_DATE"][0:4] - self.date.fixDayMonth() - def parseTrack(self, trackAPI): + self.title = trackAPI['title'] + + self.discNumber = trackAPI.get('disk_number') + self.explicit = trackAPI.get('explicit_lyrics', False) + self.copyright = trackAPI.get('copyright') + if 'gain' in trackAPI: self.replayGain = generateReplayGainString(trackAPI['gain']) + self.ISRC = trackAPI.get('isrc') + self.trackNumber = trackAPI['track_position'] + self.contributors = trackAPI.get('song_contributors') + self.rank = trackAPI['rank'] self.bpm = trackAPI['bpm'] - if not self.replayGain and 'gain' in trackAPI: - self.replayGain = generateReplayGainString(trackAPI['gain']) - if not self.explicit: - self.explicit = trackAPI['explicit_lyrics'] - if not self.discNumber: - self.discNumber = trackAPI['disk_number'] + self.lyrics = Lyrics(trackAPI.get('lyrics_id', "0")) + + self.mainArtist = Artist( + art_id = trackAPI['artist']['id'], + name = trackAPI['artist']['name'], + role = "Main", + pic_md5 = trackAPI['artist'].get('md5_image') + ) + + if 'physical_release_date' in trackAPI: + self.date.day = trackAPI["physical_release_date"][8:10] + self.date.month = trackAPI["physical_release_date"][5:7] + self.date.year = trackAPI["physical_release_date"][0:4] + self.date.fixDayMonth() for artist in trackAPI['contributors']: isVariousArtists = str(artist['id']) == VARIOUS_ARTISTS @@ -249,6 +216,11 @@ class Track: self.artist[artist['role']] = [] self.artist[artist['role']].append(artist['name']) + if 'alternative_albums' in trackAPI and trackAPI['alternative_albums']: + for album in trackAPI['alternative_albums']['data']: + if 'RIGHTS' in album and album['RIGHTS'].get('STREAM_ADS_AVAILABLE') or album['RIGHTS'].get('STREAM_SUB_AVAILABLE'): + self.albumsFallback.append(album['ALB_ID']) + def removeDuplicateArtists(self): (self.artist, self.artists) = removeDuplicateArtists(self.artist, self.artists) @@ -267,6 +239,14 @@ class Track: if 'Featured' in self.artist: self.featArtistsString = "feat. "+andCommaConcat(self.artist['Featured']) + def checkAndRenewTrackToken(self, dz): + now = datetime.now() + expiration = datetime.fromtimestamp(self.trackTokenExpiration) + if now > expiration: + newTrack = dz.gw.get_track_with_fallback(self.id) + self.trackToken = newTrack['TRACK_TOKEN'] + self.trackTokenExpiration = newTrack['TRACK_TOKEN_EXPIRE'] + def applySettings(self, settings): # Check if should save the playlist as a compilation diff --git a/setup.py b/setup.py index d647330..8ac77e9 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ setup( python_requires='>=3.7', packages=find_packages(exclude=("tests",)), include_package_data=True, - install_requires=["click", "pycryptodomex", "mutagen", "requests", "deezer-py>=1.2.8"], + install_requires=["click", "pycryptodomex", "mutagen", "requests", "deezer-py>=1.3.0"], extras_require={ "spotify": ["spotipy>=2.11.0"] },