from deemix.app.downloadjob import DownloadJob from deemix.utils import getIDFromLink, getTypeFromLink, getBitrateInt from deezer.gw import APIError as gwAPIError, LyricsStatus from deezer.api import APIError from deezer.utils import map_user_playlist from spotipy.exceptions import SpotifyException from deemix.app.queueitem import QueueItem, QISingle, QICollection, QIConvertable import logging from pathlib import Path import json from os import remove import eventlet import uuid urlopen = eventlet.import_patched('urllib.request').urlopen logging.basicConfig(level=logging.INFO) logger = logging.getLogger('deemix') class QueueManager: def __init__(self, spotifyHelper=None): self.queue = [] self.queueList = {} self.queueComplete = [] self.currentItem = "" self.sp = spotifyHelper def generateTrackQueueItem(self, dz, id, settings, bitrate, trackAPI=None, albumAPI=None): # Check if is an isrc: url if str(id).startswith("isrc"): try: trackAPI = dz.api.get_track(id) except APIError as e: e = str(e) return QueueError("https://deezer.com/track/"+str(id), f"Wrong URL: {e}") if 'id' in trackAPI and 'title' in trackAPI: id = trackAPI['id'] else: return QueueError("https://deezer.com/track/"+str(id), "Track ISRC is not available on deezer", "ISRCnotOnDeezer") # Get essential track info try: trackAPI_gw = dz.gw.get_track_with_fallback(id) except gwAPIError as e: e = str(e) message = "Wrong URL" if "DATA_ERROR" in e: message += f": {e['DATA_ERROR']}" return QueueError("https://deezer.com/track/"+str(id), message) if albumAPI: trackAPI_gw['_EXTRA_ALBUM'] = albumAPI if trackAPI: trackAPI_gw['_EXTRA_TRACK'] = trackAPI if settings['createSingleFolder']: trackAPI_gw['FILENAME_TEMPLATE'] = settings['albumTracknameTemplate'] else: trackAPI_gw['FILENAME_TEMPLATE'] = settings['tracknameTemplate'] trackAPI_gw['SINGLE_TRACK'] = True 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))) return QISingle( id=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, type='track', settings=settings, single=trackAPI_gw, ) def generateAlbumQueueItem(self, dz, id, settings, bitrate, rootArtist=None): # Get essential album info try: albumAPI = dz.api.get_album(id) except APIError as e: e = str(e) return QueueError("https://deezer.com/album/"+str(id), f"Wrong URL: {e}") if str(id).startswith('upc'): id = albumAPI['id'] # Get extra info about album # This saves extra api calls when downloading albumAPI_gw = dz.gw.get_album(id) albumAPI['nb_disk'] = albumAPI_gw['NUMBER_DISK'] albumAPI['copyright'] = albumAPI_gw['COPYRIGHT'] albumAPI['root_artist'] = rootArtist # If the album is a single download as a track if albumAPI['nb_tracks'] == 1: return self.generateTrackQueueItem(dz, albumAPI['tracks']['data'][0]['id'], settings, bitrate, albumAPI=albumAPI) tracksArray = dz.gw.get_album_tracks(id) 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) albumAPI['nb_tracks'] = totalSize 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) explicit = albumAPI_gw.get('EXPLICIT_ALBUM_CONTENT', {}).get('EXPLICIT_LYRICS_STATUS', LyricsStatus.UNKNOWN) in [LyricsStatus.EXPLICIT, LyricsStatus.PARTIALLY_EXPLICIT] return QICollection( id=id, bitrate=bitrate, title=albumAPI['title'], artist=albumAPI['artist']['name'], cover=cover, explicit=explicit, size=totalSize, type='album', settings=settings, collection=collection, ) def generatePlaylistQueueItem(self, dz, id, settings, bitrate): # Get essential playlist info try: playlistAPI = dz.api.get_playlist(id) except: playlistAPI = None # Fallback to gw api if the playlist is private if not playlistAPI: try: userPlaylist = dz.gw.get_playlist_page(id) playlistAPI = map_user_playlist(userPlaylist['DATA']) except gwAPIError as e: e = str(e) message = "Wrong URL" if "DATA_ERROR" in e: message += f": {e['DATA_ERROR']}" return QueueError("https://deezer.com/playlist/"+str(id), message) # Check if private playlist and owner if not playlistAPI.get('public', False) and playlistAPI['creator']['id'] != str(dz.current_user['id']): logger.warning("You can't download others private playlists.") return QueueError("https://deezer.com/playlist/"+str(id), "You can't download others private playlists.", "notYourPrivatePlaylist") playlistTracksAPI = dz.gw.get_playlist_tracks(id) playlistAPI['various_artist'] = dz.api.get_artist(5080) # Useful for save as compilation totalSize = len(playlistTracksAPI) 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]: 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=id, bitrate=bitrate, title=playlistAPI['title'], artist=playlistAPI['creator']['name'], cover=playlistAPI['picture_small'][:-24] + '/75x75-000000-80-0-0.jpg', explicit=playlistAPI['explicit'], size=totalSize, type='playlist', settings=settings, collection=collection, ) def generateArtistQueueItem(self, dz, id, settings, bitrate, interface=None): # Get essential artist info try: artistAPI = dz.api.get_artist(id) except APIError as e: e = str(e) return QueueError("https://deezer.com/artist/"+str(id), f"Wrong URL: {e}") if interface: interface.send("startAddingArtist", {'name': artistAPI['name'], 'id': artistAPI['id']}) rootArtist = { 'id': artistAPI['id'], 'name': artistAPI['name'] } artistDiscographyAPI = dz.gw.get_artist_discography_tabs(id, 100) allReleases = artistDiscographyAPI.pop('all', []) albumList = [] for album in allReleases: albumList.append(self.generateAlbumQueueItem(dz, album['id'], settings, bitrate, rootArtist=rootArtist)) if interface: interface.send("finishAddingArtist", {'name': artistAPI['name'], 'id': artistAPI['id']}) return albumList def generateArtistDiscographyQueueItem(self, dz, id, settings, bitrate, interface=None): # Get essential artist info try: artistAPI = dz.api.get_artist(id) except APIError as e: e = str(e) return QueueError("https://deezer.com/artist/"+str(id)+"/discography", f"Wrong URL: {e}") if interface: interface.send("startAddingArtist", {'name': artistAPI['name'], 'id': artistAPI['id']}) rootArtist = { 'id': artistAPI['id'], 'name': artistAPI['name'] } artistDiscographyAPI = dz.gw.get_artist_discography_tabs(id, 100) artistDiscographyAPI.pop('all', None) # all contains albums and singles, so its all duplicates. This removes them albumList = [] for type in artistDiscographyAPI: for album in artistDiscographyAPI[type]: albumList.append(self.generateAlbumQueueItem(dz, album['id'], settings, bitrate, rootArtist=rootArtist)) if interface: interface.send("finishAddingArtist", {'name': artistAPI['name'], 'id': artistAPI['id']}) return albumList def generateArtistTopQueueItem(self, dz, id, settings, bitrate, interface=None): # Get essential artist info try: artistAPI = dz.api.get_artist(id) except APIError as e: e = str(e) return QueueError("https://deezer.com/artist/"+str(id)+"/top_track", f"Wrong URL: {e}") # Emulate the creation of a playlist # Can't use generatePlaylistQueueItem as this is not a real playlist 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" } artistTopTracksAPI_gw = dz.gw.get_artist_toptracks(id) playlistAPI['various_artist'] = dz.api.get_artist(5080) # Useful for save as compilation totalSize = len(artistTopTracksAPI_gw) playlistAPI['nb_tracks'] = totalSize collection = [] for pos, trackAPI in enumerate(artistTopTracksAPI_gw, start=1): if trackAPI.get('EXPLICIT_TRACK_CONTENT', {}).get('EXPLICIT_LYRICS_STATUS', LyricsStatus.UNKNOWN) in [LyricsStatus.EXPLICIT, LyricsStatus.PARTIALLY_EXPLICIT]: 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=id, bitrate=bitrate, title=playlistAPI['title'], artist=playlistAPI['creator']['name'], cover=playlistAPI['picture_small'][:-24] + '/75x75-000000-80-0-0.jpg', explicit=playlistAPI['explicit'], size=totalSize, type='playlist', settings=settings, collection=collection, ) def generateQueueItem(self, dz, url, settings, bitrate=None, interface=None): bitrate = getBitrateInt(bitrate) or settings['maxBitrate'] if 'deezer.page.link' in url: url = urlopen(url).url if 'link.tospotify.com' in url: url = urlopen(url).url type = getTypeFromLink(url) id = getIDFromLink(url, type) if type == None or id == None: logger.warn("URL not recognized") return QueueError(url, "URL not recognized", "invalidURL") if type == "track": return self.generateTrackQueueItem(dz, id, settings, bitrate) elif type == "album": return self.generateAlbumQueueItem(dz, id, settings, bitrate) elif type == "playlist": return self.generatePlaylistQueueItem(dz, id, settings, bitrate) elif type == "artist": return self.generateArtistQueueItem(dz, id, settings, bitrate, interface=interface) elif type == "artistdiscography": return self.generateArtistDiscographyQueueItem(dz, id, settings, bitrate, interface=interface) elif type == "artisttop": return self.generateArtistTopQueueItem(dz, id, settings, bitrate, interface=interface) elif type.startswith("spotify") and self.sp: if not self.sp.spotifyEnabled: logger.warn("Spotify Features is not setted up correctly.") return QueueError(url, "Spotify Features is not setted up correctly.", "spotifyDisabled") if type == "spotifytrack": try: (track_id, trackAPI, _) = self.sp.get_trackid_spotify(dz, id, settings['fallbackSearch']) except SpotifyException as e: return QueueError(url, "Wrong URL: "+e.msg[e.msg.find('\n')+2:]) except Exception as e: return QueueError(url, "Something went wrong: "+str(e)) if track_id != "0": return self.generateTrackQueueItem(dz, track_id, settings, bitrate, trackAPI=trackAPI) else: logger.warn("Track not found on deezer!") return QueueError(url, "Track not found on deezer!", "trackNotOnDeezer") elif type == "spotifyalbum": try: album_id = self.sp.get_albumid_spotify(dz, id) except SpotifyException as e: return QueueError(url, "Wrong URL: "+e.msg[e.msg.find('\n')+2:]) except Exception as e: return QueueError(url, "Something went wrong: "+str(e)) if album_id != "0": return self.generateAlbumQueueItem(dz, album_id, settings, bitrate) else: logger.warn("Album not found on deezer!") return QueueError(url, "Album not found on deezer!", "albumNotOnDeezer") elif type == "spotifyplaylist": try: return self.sp.generate_playlist_queueitem(dz, id, bitrate, settings) except SpotifyException as e: return QueueError(url, "Wrong URL: "+e.msg[e.msg.find('\n')+2:]) except Exception as e: return QueueError(url, "Something went wrong: "+str(e)) logger.warn("URL not supported yet") return QueueError(url, "URL not supported yet", "unsupportedURL") def addToQueue(self, dz, url, settings, bitrate=None, interface=None, ack=None): if not dz.logged_in: if interface: interface.send("loginNeededToDownload") return False def parseLink(link): link = link.strip() if link == "": return False logger.info("Generating queue item for: "+link) item = self.generateQueueItem(dz, link, settings, bitrate, interface=interface) # Add ack to all items if type(item) is list: for i in item: if isinstance(i, QueueItem): i.ack = ack elif isinstance(item, QueueItem): item.ack = ack return item if type(url) is list: queueItem = [] request_uuid = str(uuid.uuid4()) if interface: interface.send("startGeneratingItems", {'uuid': request_uuid, 'total': len(url)}) for link in url: item = parseLink(link) if not item: continue if type(item) is list: queueItem += item else: queueItem.append(item) if interface: interface.send("finishGeneratingItems", {'uuid': request_uuid, 'total': len(queueItem)}) if not len(queueItem): return False else: queueItem = parseLink(url) if not queueItem: return False def processQueueItem(item, silent=False): if isinstance(item, QueueError): logger.error(f"[{item.link}] {item.message}") if interface: interface.send("queueError", item.toDict()) return False if item.uuid in list(self.queueList.keys()): logger.warn(f"[{item.uuid}] Already in queue, will not be added again.") if interface and not silent: interface.send("alreadyInQueue", {'uuid': item.uuid, 'title': item.title}) return False self.queue.append(item.uuid) self.queueList[item.uuid] = item logger.info(f"[{item.uuid}] Added to queue.") return True if type(queueItem) is list: slimmedItems = [] for item in queueItem: if processQueueItem(item, silent=True): slimmedItems.append(item.getSlimmedItem()) else: continue if not len(slimmedItems): return False if interface: interface.send("addedToQueue", slimmedItems) else: if processQueueItem(queueItem): if interface: interface.send("addedToQueue", queueItem.getSlimmedItem()) else: return False self.nextItem(dz, interface) return True def nextItem(self, dz, interface=None): # Check that nothing is already downloading and # that the queue is not empty if self.currentItem != "": return None if not len(self.queue): return None self.currentItem = self.queue.pop(0) if isinstance(self.queueList[self.currentItem], QIConvertable) and self.queueList[self.currentItem].extra: logger.info(f"[{self.currentItem}] Converting tracks to deezer.") self.sp.convert_spotify_playlist(dz, self.queueList[self.currentItem], interface=interface) logger.info(f"[{self.currentItem}] Tracks converted.") if interface: interface.send("startDownload", self.currentItem) logger.info(f"[{self.currentItem}] Started downloading.") DownloadJob(dz, self.queueList[self.currentItem], interface).start() 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, 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(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): configFolder = Path(configFolder) if (configFolder / 'queue.json').is_file() and not len(self.queue): if interface: interface.send('restoringQueue') with open(configFolder / 'queue.json', 'r') as f: try: qd = json.load(f) except json.decoder.JSONDecodeError: logger.warn("Saved queue is corrupted, resetting it") qd = { 'queue': [], 'queueComplete': [], 'queueList': {} } remove(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 return if uuid in self.queue: self.queue.remove(uuid) elif uuid in self.queueComplete: self.queueComplete.remove(uuid) else: return 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("removedAllDownloads", self.currentItem) def removeFinishedDownloads(self, interface=None): for uuid in self.queueComplete: del self.queueList[uuid] self.queueComplete = [] if interface: interface.send("removedFinishedDownloads") class QueueError: def __init__(self, link, message, errid=None): self.link = link self.message = message self.errid = errid def toDict(self): return { 'link': self.link, 'error': self.message, 'errid': self.errid }