deemix-py/deemix/app/queuemanager.py

543 lines
22 KiB
Python
Raw Normal View History

2020-08-15 21:03:05 +00:00
from deemix.app.downloadjob import DownloadJob
2020-09-20 08:35:05 +00:00
from deemix.utils import getIDFromLink, getTypeFromLink, getBitrateInt
2020-09-30 13:34:30 +00:00
from deemix.api.deezer import APIError, LyricsStatus
2020-06-29 11:58:06 +00:00
from spotipy.exceptions import SpotifyException
2020-09-18 16:52:05 +00:00
from deemix.app.queueitem import QueueItem, QISingle, QICollection, QIConvertable
import logging
from pathlib import Path
import json
from os import remove
2020-09-16 14:44:15 +00:00
import eventlet
urlopen = eventlet.import_patched('urllib.request').urlopen
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger('deemix')
class QueueManager:
2020-09-12 11:08:28 +00:00
def __init__(self, spotifyHelper=None):
self.queue = []
self.queueList = {}
self.queueComplete = []
self.currentItem = ""
2020-09-12 11:08:28 +00:00
self.sp = spotifyHelper
2020-09-30 13:34:30 +00:00
def generateTrackQueueItem(self, dz, id, settings, bitrate, trackAPI=None, albumAPI=None):
2020-09-30 14:19:14 +00:00
if str(id).startswith("isrc"):
try:
2020-09-30 13:34:30 +00:00
trackAPI = dz.get_track(id)
except APIError as e:
e = json.loads(str(e))
2020-09-30 13:34:30 +00:00
return QueueError("https://deezer.com/track/"+str(id), f"Wrong URL: {e['type']+': ' if 'type' in e else ''}{e['message'] if 'message' in e else ''}")
if 'id' in trackAPI and 'title' in trackAPI:
id = trackAPI['id']
else:
2020-09-30 13:34:30 +00:00
return QueueError("https://deezer.com/track/"+str(id), "Track ISRC is not available on deezer", "ISRCnotOnDeezer")
try:
trackAPI_gw = dz.get_track_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("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']
if 'VERSION' in trackAPI_gw and trackAPI_gw['VERSION']:
title += " " + trackAPI_gw['VERSION']
explicit = int(trackAPI_gw['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):
try:
albumAPI = dz.get_album(id)
except APIError as e:
e = json.loads(str(e))
return QueueError("https://deezer.com/album/"+str(id), f"Wrong URL: {e['type']+': ' if 'type' in e else ''}{e['message'] if 'message' in e else ''}")
2020-09-30 14:19:14 +00:00
if str(id).startswith('upc'):
2020-09-30 13:34:30 +00:00
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 self.generateTrackQueueItem(dz, albumAPI['tracks']['data'][0]['id'], settings, bitrate, albumAPI=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)
explicit = albumAPI_gw['EXPLICIT_ALBUM_CONTENT']['EXPLICIT_LYRICS_STATUS'] 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):
try:
playlistAPI = dz.get_playlist(id)
except:
playlistAPI = None
if not playlistAPI:
2020-06-29 11:58:06 +00:00
try:
2020-09-30 13:34:30 +00:00
playlistAPI = dz.get_playlist_gw(id)
2020-06-29 11:58:06 +00:00
except APIError as e:
e = json.loads(str(e))
2020-09-30 13:34:30 +00:00
message = "Wrong URL"
if "DATA_ERROR" in e:
message += f": {e['DATA_ERROR']}"
return QueueError("https://deezer.com/playlist/"+str(id), message)
if not playlistAPI['public'] and playlistAPI['creator']['id'] != str(dz.user['id']):
logger.warn("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.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 trackAPI['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):
try:
artistAPI = dz.get_artist(id)
except APIError as e:
e = json.loads(str(e))
return QueueError("https://deezer.com/artist/"+str(id), 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(self.generateAlbumQueueItem(dz, album['id'], settings, bitrate))
if interface: interface.send("finishAddingArtist", {'name': artistAPI['name'], 'id': artistAPI['id']})
return albumList
def generateArtistDiscographyQueueItem(self, dz, id, settings, bitrate, interface=None):
try:
artistAPI = dz.get_artist(id)
except APIError as e:
e = json.loads(str(e))
return QueueError("https://deezer.com/artist/"+str(id)+"/discography", 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(self.generateAlbumQueueItem(dz, album['id'], settings, bitrate))
if interface: interface.send("finishAddingArtist", {'name': artistAPI['name'], 'id': artistAPI['id']})
return albumList
def generateArtistTopQueueItem(self, dz, id, settings, bitrate, interface=None):
try:
artistAPI = dz.get_artist(id)
except APIError as e:
e = json.loads(str(e))
return QueueError("https://deezer.com/artist/"+str(id)+"/top_track", 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': 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.get_artist_toptracks_gw(id)
playlistAPI['various_artist'] = dz.get_artist(5080)
playlistAPI['nb_tracks'] = len(artistTopTracksAPI_gw)
totalSize = len(artistTopTracksAPI_gw)
collection = []
for pos, trackAPI in enumerate(artistTopTracksAPI_gw, start=1):
if trackAPI['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
2020-09-30 13:34:30 +00:00
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")
2020-09-30 13:34:30 +00:00
if type == "track":
return self.generateTrackQueueItem(dz, id, settings, bitrate)
elif type == "album":
return self.generateAlbumQueueItem(dz, id, settings, bitrate)
elif type == "playlist":
2020-09-30 13:34:30 +00:00
return self.generatePlaylistQueueItem(dz, id, settings, bitrate)
elif type == "artist":
2020-09-30 13:34:30 +00:00
return self.generateArtistQueueItem(dz, id, settings, bitrate, interface=interface)
elif type == "artistdiscography":
2020-09-30 13:34:30 +00:00
return self.generateArtistDiscographyQueueItem(dz, id, settings, bitrate, interface=interface)
elif type == "artisttop":
2020-09-30 13:34:30 +00:00
return self.generateArtistTopQueueItem(dz, id, settings, bitrate, interface=interface)
elif type.startswith("spotify") and self.sp:
2020-09-12 11:08:28 +00:00
if not self.sp.spotifyEnabled:
logger.warn("Spotify Features is not setted up correctly.")
2020-08-14 20:28:26 +00:00
return QueueError(url, "Spotify Features is not setted up correctly.", "spotifyDisabled")
2020-09-30 13:34:30 +00:00
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")
2020-09-30 13:34:30 +00:00
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")
2020-09-30 13:34:30 +00:00
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))
else:
logger.warn("URL not supported yet")
2020-08-14 20:28:26 +00:00
return QueueError(url, "URL not supported yet", "unsupportedURL")
def addToQueue(self, dz, url, settings, bitrate=None, interface=None, ack=None):
2020-08-14 20:28:26 +00:00
if not dz.logged_in:
2020-09-30 13:34:30 +00:00
if interface: interface.send("loginNeededToDownload")
2020-08-14 20:28:26 +00:00
return False
2020-08-15 21:03:05 +00:00
2020-08-14 20:28:26 +00:00
def parseLink(link):
link = link.strip()
if link == "":
2020-08-14 20:28:26 +00:00
return False
logger.info("Generating queue item for: "+link)
item = self.generateQueueItem(dz, link, settings, bitrate, interface=interface)
2020-09-30 13:34:30 +00:00
# Add ack to all items
2020-09-18 16:52:05 +00:00
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
2020-08-15 21:03:05 +00:00
2020-08-14 20:28:26 +00:00
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
2020-08-15 21:03:05 +00:00
2020-09-30 13:34:30 +00:00
def processQueueItem(item, silenced=False):
if isinstance(item, QueueError):
logger.error(f"[{item.link}] {item.message}")
if interface and not silenced: 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 silenced: 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
2020-08-14 20:28:26 +00:00
if type(queueItem) is list:
slimmedItems = []
2020-09-30 13:34:30 +00:00
for item in queueItem:
if processQueueItem(item, silenced=True):
slimmedItems.append(item.getSlimmedItem())
else:
2020-08-14 20:28:26 +00:00
continue
2020-09-30 13:34:30 +00:00
if not len(slimmedItems):
2020-08-14 20:28:26 +00:00
return False
2020-09-30 13:34:30 +00:00
if interface: interface.send("addedToQueue", slimmedItems)
2020-08-14 20:28:26 +00:00
else:
2020-09-30 13:34:30 +00:00
if processQueueItem(queueItem):
if interface: interface.send("addedToQueue", queueItem.getSlimmedItem())
else:
2020-08-14 20:28:26 +00:00
return False
2020-09-12 11:08:28 +00:00
self.nextItem(dz, interface)
2020-08-14 20:28:26 +00:00
return True
2020-09-12 11:08:28 +00:00
def nextItem(self, dz, interface=None):
2020-08-14 20:28:26 +00:00
if self.currentItem != "":
return None
else:
if len(self.queue) > 0:
self.currentItem = self.queue.pop(0)
else:
return None
2020-09-30 13:34:30 +00:00
2020-09-12 11:08:28 +00:00
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.")
2020-09-30 13:34:30 +00:00
if interface: interface.send("startDownload", self.currentItem)
2020-08-14 20:28:26 +00:00
logger.info(f"[{self.currentItem}] Started downloading.")
2020-09-12 11:08:28 +00:00
DownloadJob(dz, self.queueList[self.currentItem], interface).start()
2020-09-30 13:34:30 +00:00
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)
2020-04-11 13:43:59 +00:00
2020-08-14 20:28:26 +00:00
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:
qd = json.load(f)
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):
2020-08-14 20:28:26 +00:00
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
2020-08-14 20:28:26 +00:00
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)
2020-08-14 20:28:26 +00:00
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:
2020-08-14 20:28:26 +00:00
interface.send("removedAllDownloads", self.currentItem)
2020-08-14 20:28:26 +00:00
def removeFinishedDownloads(self, interface=None):
for uuid in self.queueComplete:
2020-08-16 22:33:13 +00:00
del self.queueList[uuid]
2020-08-14 20:28:26 +00:00
self.queueComplete = []
if interface:
2020-08-14 20:28:26 +00:00
interface.send("removedFinishedDownloads")
class QueueError:
def __init__(self, link, message, errid=None):
self.link = link
self.message = message
self.errid = errid
2020-08-14 20:28:26 +00:00
def toDict(self):
return {
'link': self.link,
'error': self.message,
'errid': self.errid
}