from deemix.app.downloadjob import DownloadJob from deemix.utils import getIDFromLink, getTypeFromLink, getBitrateInt from deezer import Deezer 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 uuid from urllib.request import urlopen import threading logging.basicConfig(level=logging.INFO) logger = logging.getLogger('deemix') class QueueManager: def __init__(self, deezerHelper=None, spotifyHelper=None): self.queue = [] self.queueList = {} self.queueComplete = [] self.currentItem = "" self.dz = deezerHelper or Deezer() self.sp = spotifyHelper self.queueThread = None def generateTrackQueueItem(self, id, settings, bitrate, trackAPI=None, albumAPI=None, dz=None): if not dz: dz = self.dz # 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, id, settings, bitrate, rootArtist=None, dz=None): if not dz: dz = self.dz # 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(albumAPI['tracks']['data'][0]['id'], settings, bitrate, albumAPI=albumAPI, dz=dz) 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, id, settings, bitrate, dz=None): if not dz: dz = self.dz # 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, id, settings, bitrate, dz=None, interface=None): if not dz: dz = self.dz # 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(album['id'], settings, bitrate, rootArtist=rootArtist, dz=dz)) if interface: interface.send("finishAddingArtist", {'name': artistAPI['name'], 'id': artistAPI['id']}) return albumList def generateArtistDiscographyQueueItem(self, id, settings, bitrate, dz=None, interface=None): if not dz: dz = self.dz # 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(album['id'], settings, bitrate, rootArtist=rootArtist, dz=dz)) if interface: interface.send("finishAddingArtist", {'name': artistAPI['name'], 'id': artistAPI['id']}) return albumList def generateArtistTopQueueItem(self, id, settings, bitrate, dz=None, interface=None): if not dz: dz = self.dz # 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, url, settings, bitrate=None, dz=None, interface=None): if not dz: dz = self.dz 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(id, settings, bitrate, dz=dz) elif type == "album": return self.generateAlbumQueueItem(id, settings, bitrate, dz=dz) elif type == "playlist": return self.generatePlaylistQueueItem(id, settings, bitrate, dz=dz) elif type == "artist": return self.generateArtistQueueItem(id, settings, bitrate, interface=interface, dz=dz) elif type == "artistdiscography": return self.generateArtistDiscographyQueueItem(id, settings, bitrate, interface=interface, dz=dz) elif type == "artisttop": return self.generateArtistTopQueueItem(id, settings, bitrate, interface=interface, dz=dz) 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(track_id, settings, bitrate, trackAPI=trackAPI, dz=dz) 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(album_id, settings, bitrate, dz=dz) 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, url, settings, bitrate=None, dz=None, interface=None, ack=None): if not dz: dz = self.dz 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(link, settings, bitrate, interface=interface, dz=dz) # 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.startQueue(interface, dz) return True def nextItem(self, dz=None, interface=None): if not dz: dz = self.dz # Check that nothing is already downloading and # that the queue is not empty if self.currentItem != "" or not len(self.queue): self.queueThread = None 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 startQueue(self, interface=None, dz=None): if not dz: dz = self.dz if dz.logged_in and not self.queueThread: self.queueThread = threading.Thread(target=self.nextItem, args=(dz, interface)) self.queueThread.start() 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 }