Continued code reworking

This commit is contained in:
RemixDev 2020-08-14 22:28:26 +02:00
parent c611420bd9
commit 243cf3dfa6
4 changed files with 194 additions and 198 deletions

View File

@ -257,7 +257,36 @@ class Deezer:
return self.gw_api_call('deezer.pageArtist', {'art_id': art_id}) return self.gw_api_call('deezer.pageArtist', {'art_id': art_id})
def get_playlist_gw(self, playlist_id): def get_playlist_gw(self, playlist_id):
return self.gw_api_call('deezer.pagePlaylist', {'playlist_id': playlist_id, 'lang': 'en'}) playlistAPI = self.gw_api_call('deezer.pagePlaylist', {'playlist_id': playlist_id, 'lang': 'en'})['results']['DATA']
print(json.dumps(playlistAPI))
return {
'id': playlistAPI['PLAYLIST_ID'],
'title': playlistAPI['TITLE'],
'description': playlistAPI['DESCRIPTION'],
'duration': playlistAPI['DURATION'],
'public': playlistAPI['STATUS'] == 1,
'is_loved_track': playlistAPI['TYPE'] == 4,
'collaborative': playlistAPI['STATUS'] == 2,
'nb_tracks': playlistAPI['NB_SONG'],
'fans': playlistAPI['NB_FAN'],
'link': "https://www.deezer.com/playlist/"+playlistAPI['PLAYLIST_ID'],
'share': "https://www.deezer.com/playlist/"+playlistAPI['PLAYLIST_ID'],
'picture': "https://api.deezer.com/playlist/"+playlistAPI['PLAYLIST_ID']+"/image",
'picture_small': "https://cdns-images.dzcdn.net/images/"+playlistAPI['PICTURE_TYPE']+"/"+playlistAPI['PLAYLIST_PICTURE']+"/56x56-000000-80-0-0.jpg",
'picture_medium': "https://cdns-images.dzcdn.net/images/"+playlistAPI['PICTURE_TYPE']+"/"+playlistAPI['PLAYLIST_PICTURE']+"/250x250-000000-80-0-0.jpg",
'picture_big': "https://cdns-images.dzcdn.net/images/"+playlistAPI['PICTURE_TYPE']+"/"+playlistAPI['PLAYLIST_PICTURE']+"/500x500-000000-80-0-0.jpg",
'picture_xl': "https://cdns-images.dzcdn.net/images/"+playlistAPI['PICTURE_TYPE']+"/"+playlistAPI['PLAYLIST_PICTURE']+"/1000x1000-000000-80-0-0.jpg",
'checksum': playlistAPI['CHECKSUM'],
'tracklist': "https://api.deezer.com/playlist/"+playlistAPI['PLAYLIST_ID']+"/tracks",
'creation_date': playlistAPI['DATE_ADD'],
'creator': {
'id': playlistAPI['PARENT_USER_ID'],
'name': playlistAPI['PARENT_USERNAME'],
'tracklist': "https://api.deezer.com/user/"+playlistAPI['PARENT_USER_ID']+"/flow",
'type': "user"
},
'type': "playlist"
}
def get_playlist_tracks_gw(self, playlist_id): def get_playlist_tracks_gw(self, playlist_id):
tracks_array = [] tracks_array = []

View File

@ -1,21 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""
queueItem base structure
title
artist
cover
size
downloaded
failed
errors
progress
type
id
bitrate
uuid: type+id+bitrate
"""
class QueueItem: class QueueItem:
def __init__(self, id=None, bitrate=None, title=None, artist=None, cover=None, size=None, type=None, settings=None, queueItemList=None): def __init__(self, id=None, bitrate=None, title=None, artist=None, cover=None, size=None, type=None, settings=None, queueItemList=None):
if queueItemList: if queueItemList:
@ -41,9 +25,10 @@ class QueueItem:
self.errors = [] self.errors = []
self.progress = 0 self.progress = 0
self.uuid = f"{self.type}_{self.id}_{self.bitrate}" self.uuid = f"{self.type}_{self.id}_{self.bitrate}"
self.cancel = False
def toDict(self): def toDict(self):
queueItem = { return {
'title': self.title, 'title': self.title,
'artist': self.artist, 'artist': self.artist,
'cover': self.cover, 'cover': self.cover,
@ -57,7 +42,6 @@ class QueueItem:
'bitrate': self.bitrate, 'bitrate': self.bitrate,
'uuid': self.uuid 'uuid': self.uuid
} }
return queueItem
def getResettedItem(self): def getResettedItem(self):
item = self.toDict() item = self.toDict()
@ -102,3 +86,17 @@ class QICollection(QueueItem):
queueItem = super().toDict() queueItem = super().toDict()
queueItem['collection'] = self.collection queueItem['collection'] = self.collection
return queueItem return queueItem
class QIConvertable(QueueItem):
def __init__(self, id=None, bitrate=None, title=None, artist=None, cover=None, size=None, type=None, settings=None, extra=None, queueItemList=None):
if queueItemList:
super().__init__(queueItemList=queueItemList)
self.extra = queueItemList['_EXTRA']
else:
super().__init__(id, bitrate, title, artist, cover, size, type, settings)
self.extra = extra
def toDict(self):
queueItem = super().toDict()
queueItem['_EXTRA'] = self.extra
return queueItem

View File

@ -26,7 +26,7 @@ class QueueManager:
if type == None or id == None: if type == None or id == None:
logger.warn("URL not recognized") logger.warn("URL not recognized")
return queueError(url, "URL not recognized", "invalidURL") return QueueError(url, "URL not recognized", "invalidURL")
elif type == "track": elif type == "track":
if id.startswith("isrc"): if id.startswith("isrc"):
@ -38,7 +38,7 @@ class QueueManager:
except APIError as e: except APIError as e:
e = json.loads(str(e)) e = json.loads(str(e))
return queueError(url, f"Wrong URL: {e['type']+': ' if 'type' in e else ''}{e['message'] if 'message' in e else ''}") return QueueError(url, f"Wrong URL: {e['type']+': ' if 'type' in e else ''}{e['message'] if 'message' in e else ''}")
try: try:
trackAPI = dz.get_track_gw(id) trackAPI = dz.get_track_gw(id)
except APIError as e: except APIError as e:
@ -46,7 +46,7 @@ class QueueManager:
message = "Wrong URL" message = "Wrong URL"
if "DATA_ERROR" in e: if "DATA_ERROR" in e:
message += f": {e['DATA_ERROR']}" message += f": {e['DATA_ERROR']}"
return queueError(url, message) return QueueError(url, message)
if albumAPI: if albumAPI:
trackAPI['_EXTRA_ALBUM'] = albumAPI trackAPI['_EXTRA_ALBUM'] = albumAPI
if settings['createSingleFolder']: if settings['createSingleFolder']:
@ -74,7 +74,7 @@ class QueueManager:
albumAPI = dz.get_album(id) albumAPI = dz.get_album(id)
except APIError as e: except APIError as e:
e = json.loads(str(e)) e = json.loads(str(e))
return queueError(url, f"Wrong URL: {e['type']+': ' if 'type' in e else ''}{e['message'] if 'message' in e else ''}") return QueueError(url, f"Wrong URL: {e['type']+': ' if 'type' in e else ''}{e['message'] if 'message' in e else ''}")
if id.startswith('upc'): if id.startswith('upc'):
id = albumAPI['id'] id = albumAPI['id']
albumAPI_gw = dz.get_album_gw(id) albumAPI_gw = dz.get_album_gw(id)
@ -119,45 +119,16 @@ class QueueManager:
playlistAPI = dz.get_playlist(id) playlistAPI = dz.get_playlist(id)
except: except:
try: try:
playlistAPI = dz.get_playlist_gw(id)['results']['DATA'] playlistAPI = dz.get_playlist_gw(id)
except APIError as e: except APIError as e:
e = json.loads(str(e)) e = json.loads(str(e))
message = "Wrong URL" message = "Wrong URL"
if "DATA_ERROR" in e: if "DATA_ERROR" in e:
message += f": {e['DATA_ERROR']}" message += f": {e['DATA_ERROR']}"
return queueError(url, message) return QueueError(url, message)
newPlaylist = {
'id': playlistAPI['PLAYLIST_ID'],
'title': playlistAPI['TITLE'],
'description': playlistAPI['DESCRIPTION'],
'duration': playlistAPI['DURATION'],
'public': False,
'is_loved_track': False,
'collaborative': False,
'nb_tracks': playlistAPI['NB_SONG'],
'fans': playlistAPI['NB_FAN'],
'link': "https://www.deezer.com/playlist/"+playlistAPI['PLAYLIST_ID'],
'share': None,
'picture': "https://api.deezer.com/playlist/"+playlistAPI['PLAYLIST_ID']+"/image",
'picture_small': "https://cdns-images.dzcdn.net/images/"+playlistAPI['PICTURE_TYPE']+"/"+playlistAPI['PLAYLIST_PICTURE']+"/56x56-000000-80-0-0.jpg",
'picture_medium': "https://cdns-images.dzcdn.net/images/"+playlistAPI['PICTURE_TYPE']+"/"+playlistAPI['PLAYLIST_PICTURE']+"/250x250-000000-80-0-0.jpg",
'picture_big': "https://cdns-images.dzcdn.net/images/"+playlistAPI['PICTURE_TYPE']+"/"+playlistAPI['PLAYLIST_PICTURE']+"/500x500-000000-80-0-0.jpg",
'picture_xl': "https://cdns-images.dzcdn.net/images/"+playlistAPI['PICTURE_TYPE']+"/"+playlistAPI['PLAYLIST_PICTURE']+"/1000x1000-000000-80-0-0.jpg",
'checksum': playlistAPI['CHECKSUM'],
'tracklist': "https://api.deezer.com/playlist/"+playlistAPI['PLAYLIST_ID']+"/tracks",
'creation_date': playlistAPI['DATE_ADD'],
'creator': {
'id': playlistAPI['PARENT_USER_ID'],
'name': playlistAPI['PARENT_USERNAME'],
'tracklist': "https://api.deezer.com/user/"+playlistAPI['PARENT_USER_ID']+"/flow",
'type': "user"
},
'type': "playlist"
}
playlistAPI = newPlaylist
if not playlistAPI['public'] and playlistAPI['creator']['id'] != str(dz.user['id']): if not playlistAPI['public'] and playlistAPI['creator']['id'] != str(dz.user['id']):
logger.warn("You can't download others private playlists.") logger.warn("You can't download others private playlists.")
return return queueError(url, "You can't download others private playlists.", "notYourPrivatePlaylist") return return QueueError(url, "You can't download others private playlists.", "notYourPrivatePlaylist")
playlistTracksAPI = dz.get_playlist_tracks_gw(id) playlistTracksAPI = dz.get_playlist_tracks_gw(id)
playlistAPI['various_artist'] = dz.get_artist(5080) playlistAPI['various_artist'] = dz.get_artist(5080)
@ -192,7 +163,7 @@ class QueueManager:
artistAPI = dz.get_artist(id) artistAPI = dz.get_artist(id)
except APIError as e: except APIError as e:
e = json.loads(str(e)) e = json.loads(str(e))
return return queueError(url, f"Wrong URL: {e['type']+': ' if 'type' in e else ''}{e['message'] if 'message' in e else ''}") return return QueueError(url, f"Wrong URL: {e['type']+': ' if 'type' in e else ''}{e['message'] if 'message' in e else ''}")
if interface: if interface:
interface.send("startAddingArtist", {'name': artistAPI['name'], 'id': artistAPI['id']}) interface.send("startAddingArtist", {'name': artistAPI['name'], 'id': artistAPI['id']})
@ -212,7 +183,7 @@ class QueueManager:
artistAPI = dz.get_artist(id) artistAPI = dz.get_artist(id)
except APIError as e: except APIError as e:
e = json.loads(str(e)) e = json.loads(str(e))
return return queueError(url, f"Wrong URL: {e['type']+': ' if 'type' in e else ''}{e['message'] if 'message' in e else ''}") return return QueueError(url, f"Wrong URL: {e['type']+': ' if 'type' in e else ''}{e['message'] if 'message' in e else ''}")
if interface: if interface:
interface.send("startAddingArtist", {'name': artistAPI['name'], 'id': artistAPI['id']}) interface.send("startAddingArtist", {'name': artistAPI['name'], 'id': artistAPI['id']})
@ -234,7 +205,7 @@ class QueueManager:
artistAPI = dz.get_artist(id) artistAPI = dz.get_artist(id)
except APIError as e: except APIError as e:
e = json.loads(str(e)) e = json.loads(str(e))
return return queueError(url, f"Wrong URL: {e['type']+': ' if 'type' in e else ''}{e['message'] if 'message' in e else ''}") return return QueueError(url, f"Wrong URL: {e['type']+': ' if 'type' in e else ''}{e['message'] if 'message' in e else ''}")
playlistAPI = { playlistAPI = {
'id': str(artistAPI['id'])+"_top_track", 'id': str(artistAPI['id'])+"_top_track",
@ -296,39 +267,39 @@ class QueueManager:
elif type == "spotifytrack": elif type == "spotifytrack":
if not sp.spotifyEnabled: if not sp.spotifyEnabled:
logger.warn("Spotify Features is not setted up correctly.") logger.warn("Spotify Features is not setted up correctly.")
return queueError(url, "Spotify Features is not setted up correctly.", "spotifyDisabled") return QueueError(url, "Spotify Features is not setted up correctly.", "spotifyDisabled")
try: try:
track_id = sp.get_trackid_spotify(dz, id, settings['fallbackSearch']) track_id = sp.get_trackid_spotify(dz, id, settings['fallbackSearch'])
except SpotifyException as e: except SpotifyException as e:
return queueError(url, "Wrong URL: "+e.msg[e.msg.find('\n')+2:]) return QueueError(url, "Wrong URL: "+e.msg[e.msg.find('\n')+2:])
if track_id != 0: if track_id != 0:
return generateQueueItem(dz, sp, f'https://www.deezer.com/track/{track_id}', settings, bitrate) return generateQueueItem(dz, sp, f'https://www.deezer.com/track/{track_id}', settings, bitrate)
else: else:
logger.warn("Track not found on deezer!") logger.warn("Track not found on deezer!")
return queueError(url, "Track not found on deezer!", "trackNotOnDeezer") return QueueError(url, "Track not found on deezer!", "trackNotOnDeezer")
elif type == "spotifyalbum": elif type == "spotifyalbum":
if not sp.spotifyEnabled: if not sp.spotifyEnabled:
logger.warn("Spotify Features is not setted up correctly.") logger.warn("Spotify Features is not setted up correctly.")
return queueError(url, "Spotify Features is not setted up correctly.", "spotifyDisabled") return QueueError(url, "Spotify Features is not setted up correctly.", "spotifyDisabled")
try: try:
album_id = sp.get_albumid_spotify(dz, id) album_id = sp.get_albumid_spotify(dz, id)
except SpotifyException as e: except SpotifyException as e:
return queueError(url, "Wrong URL: "+e.msg[e.msg.find('\n')+2:]) return QueueError(url, "Wrong URL: "+e.msg[e.msg.find('\n')+2:])
if album_id != 0: if album_id != 0:
return generateQueueItem(dz, sp, f'https://www.deezer.com/album/{album_id}', settings, bitrate) return generateQueueItem(dz, sp, f'https://www.deezer.com/album/{album_id}', settings, bitrate)
else: else:
logger.warn("Album not found on deezer!") logger.warn("Album not found on deezer!")
return queueError(url, "Album not found on deezer!", "albumNotOnDeezer") return QueueError(url, "Album not found on deezer!", "albumNotOnDeezer")
elif type == "spotifyplaylist": elif type == "spotifyplaylist":
if not sp.spotifyEnabled: if not sp.spotifyEnabled:
logger.warn("Spotify Features is not setted up correctly.") logger.warn("Spotify Features is not setted up correctly.")
return queueError(url, "Spotify Features is not setted up correctly.", "spotifyDisabled") return QueueError(url, "Spotify Features is not setted up correctly.", "spotifyDisabled")
try: try:
playlist = sp.adapt_spotify_playlist(dz, id, settings) playlist = sp.adapt_spotify_playlist(dz, id, settings)
@ -336,156 +307,153 @@ class QueueManager:
playlist['uuid'] = f"{playlist['type']}_{id}_{bitrate}" playlist['uuid'] = f"{playlist['type']}_{id}_{bitrate}"
return playlist return playlist
except SpotifyException as e: except SpotifyException as e:
return queueError(url, "Wrong URL: "+e.msg[e.msg.find('\n')+2:]) return QueueError(url, "Wrong URL: "+e.msg[e.msg.find('\n')+2:])
else: else:
logger.warn("URL not supported yet") logger.warn("URL not supported yet")
return queueError(url, "URL not supported yet", "unsupportedURL") return QueueError(url, "URL not supported yet", "unsupportedURL")
def addToQueue(self, dz, sp, url, settings, bitrate=None, interface=None):
def addToQueue(dz, sp, url, settings, bitrate=None, interface=None):
global currentItem, queueList, queue
if not dz.logged_in: if not dz.logged_in:
return "Not 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)
return self.generateQueueItem(dz, sp, link, settings, bitrate, interface=interface)
if type(url) is list: if type(url) is list:
queueItem = [] queueItem = []
for link in url: for link in url:
link = link.strip() item = parseLink(link)
if link == "": if not item:
continue continue
logger.info("Generating queue item for: "+link) elif type(item) is list:
item = generateQueueItem(dz, sp, link, settings, bitrate, interface=interface)
if type(item) is list:
queueItem += item queueItem += item
else: else:
queueItem.append(item) queueItem.append(item)
else: if not len(queueItem):
url = url.strip() return False
if url == "": else:
queueItem = parseLink(url)
if not queueItem:
return False return False
logger.info("Generating queue item for: "+url)
queueItem = generateQueueItem(dz, sp, url, settings, bitrate, interface=interface)
if type(queueItem) is list: if type(queueItem) is list:
ogLen = len(self.queue)
for x in queueItem: for x in queueItem:
if 'error' in x: if isinstance(x, QueueError):
logger.error(f"[{x['link']}] {x['error']}") logger.error(f"[{x.link}] {x.message}")
continue continue
if x['uuid'] in list(queueList.keys()): if x.uuid in list(self.queueList.keys()):
logger.warn(f"[{x['uuid']}] Already in queue, will not be added again.") logger.warn(f"[{x.uuid}] Already in queue, will not be added again.")
continue continue
if interface: self.queue.append(x.uuid)
interface.send("addedToQueue", slimQueueItem(x)) self.queueList[x.uuid] = x
queue.append(x['uuid']) logger.info(f"[{x.uuid}] Added to queue.")
queueList[x['uuid']] = x if ogLen <= len(self.queue):
logger.info(f"[{x['uuid']}] Added to queue.") return False
else: else:
if 'error' in queueItem: if isinstance(queueItem, QueueError):
logger.error(f"[{queueItem['link']}] {queueItem['error']}") logger.error(f"[{x.link}] {x.message}")
if interface: if interface:
interface.send("queueError", queueItem) interface.send("queueError", queueItem.toDict())
return False return False
if queueItem['uuid'] in list(queueList.keys()): if queueItem.uuid in list(self.queueList.keys()):
logger.warn(f"[{queueItem['uuid']}] Already in queue, will not be added again.") logger.warn(f"[{queueItem.uuid}] Already in queue, will not be added again.")
if interface: if interface:
interface.send("alreadyInQueue", {'uuid': queueItem['uuid'], 'title': queueItem['title']}) interface.send("alreadyInQueue", {'uuid': queueItem.uuid, 'title': queueItem.title})
return False return False
if interface: if interface:
interface.send("addedToQueue", slimQueueItem(queueItem)) interface.send("addedToQueue", queueItem.getSlimmedItem())
logger.info(f"[{queueItem['uuid']}] Added to queue.") logger.info(f"[{queueItem.uuid}] Added to queue.")
queue.append(queueItem['uuid']) self.queue.append(queueItem.uuid)
queueList[queueItem['uuid']] = queueItem self.queueList[queueItem.uuid] = queueItem
nextItem(dz, sp, interface) self.nextItem(dz, sp, interface)
return True return True
def nextItem(self, dz, sp, interface=None):
def nextItem(dz, sp, interface=None): if self.currentItem != "":
global currentItem, queueList, queue
if currentItem != "":
return None return None
else: else:
if len(queue) > 0: if len(self.queue) > 0:
currentItem = queue.pop(0) self.currentItem = self.queue.pop(0)
else: else:
return None return None
if interface: if interface:
interface.send("startDownload", currentItem) interface.send("startDownload", self.currentItem)
logger.info(f"[{currentItem}] Started downloading.") logger.info(f"[{self.currentItem}] Started downloading.")
result = download(dz, sp, queueList[currentItem], interface) download(dz, sp, self.queueList[self.currentItem], interface)
callbackQueueDone(result) self.afterDownload(dz, sp, interface)
def afterDownload(self, dz, sp, interface):
def callbackQueueDone(result): if self.queueList[self.currentItem].cancel:
global currentItem, queueList, queueComplete del self.queueList[self.currentItem]
if 'cancel' in queueList[currentItem]:
del queueList[currentItem]
else: else:
queueComplete.append(currentItem) self.queueComplete.append(self.currentItem)
logger.info(f"[{currentItem}] Finished downloading.") logger.info(f"[{self.currentItem}] Finished downloading.")
currentItem = "" self.currentItem = ""
nextItem(result['dz'], result['sp'], result['interface']) self.nextItem(dz, sp, interface)
def getQueue(): def getQueue(self):
global currentItem, queueList, queue, queueComplete return (self.queue, self.queueComplete, self.queueList, self.currentItem)
return (queue, queueComplete, queueList, currentItem)
# TODO: Convert dicts to QueueItem Objects when restoring
def restoreQueue(self, queue, queueComplete, queueList, dz, sp, interface):
self.queueComplete = queueComplete
self.queueList = queueList
self.queue = queue
nextItem(dz, sp, interface)
def restoreQueue(pqueue, pqueueComplete, pqueueList, dz, interface): def removeFromQueue(self, uuid, interface=None):
global currentItem, queueList, queue, queueComplete if uuid == self.currentItem:
queueComplete = pqueueComplete
queueList = pqueueList
queue = pqueue
nextItem(dz, interface)
def removeFromQueue(uuid, interface=None):
global currentItem, queueList, queue, queueComplete
if uuid == currentItem:
if interface: if interface:
interface.send("cancellingCurrentItem", currentItem) interface.send("cancellingCurrentItem", uuid)
queueList[uuid]['cancel'] = True self.queueList[uuid].cancel = True
elif uuid in queue: elif uuid in self.queue:
queue.remove(uuid) self.queue.remove(uuid)
del queueList[uuid] del self.queueList[uuid]
if interface: if interface:
interface.send("removedFromQueue", uuid) interface.send("removedFromQueue", uuid)
elif uuid in queueComplete: elif uuid in self.queueComplete:
queueComplete.remove(uuid) self.queueComplete.remove(uuid)
del queueList[uuid] del self.queueList[uuid]
if interface: if interface:
interface.send("removedFromQueue", uuid) interface.send("removedFromQueue", uuid)
def cancelAllDownloads(interface=None): def cancelAllDownloads(self, interface=None):
global currentItem, queueList, queue, queueComplete self.queue = []
queue = [] self.queueComplete = []
queueComplete = [] if self.currentItem != "":
if currentItem != "":
if interface: if interface:
interface.send("cancellingCurrentItem", currentItem) interface.send("cancellingCurrentItem", self.currentItem)
queueList[currentItem]['cancel'] = True self.queueList[self.currentItem].cancel = True
for uuid in list(queueList.keys()): for uuid in list(self.queueList.keys()):
if uuid != currentItem: if uuid != self.currentItem:
del queueList[uuid] del self.queueList[uuid]
if interface: if interface:
interface.send("removedAllDownloads", currentItem) interface.send("removedAllDownloads", self.currentItem)
def removeFinishedDownloads(interface=None): def removeFinishedDownloads(self, interface=None):
global queueList, queueComplete for uuid in self.queueComplete:
for uuid in queueComplete: del self.queueList[self.uuid]
del queueList[uuid] self.queueComplete = []
queueComplete = []
if interface: if interface:
interface.send("removedFinishedDownloads") interface.send("removedFinishedDownloads")
class queueError: class QueueError:
def __init__(self, link, message, errid=None): def __init__(self, link, message, errid=None):
self.link = link self.link = link
self.message = message self.message = message
self.errid = errid self.errid = errid
def toList(self): def toDict(self):
error = { return {
'link' 'link': self.link,
'error': self.message,
'errid': self.errid
} }

View File

@ -6,6 +6,7 @@ from os import mkdir
import spotipy import spotipy
from spotipy.oauth2 import SpotifyClientCredentials from spotipy.oauth2 import SpotifyClientCredentials
from deemix.utils.localpaths import getConfigFolder from deemix.utils.localpaths import getConfigFolder
from deemix.app.queueitem import QIConvertable
class SpotifyHelper: class SpotifyHelper: