Implemented spotify plugin back
This commit is contained in:
parent
c47e394039
commit
8b7417f8c4
|
@ -373,6 +373,10 @@ class Downloader:
|
|||
|
||||
def downloadWrapper(self, extraData, track=None):
|
||||
trackAPI_gw = extraData['trackAPI_gw']
|
||||
if ('_EXTRA_TRACK' in trackAPI_gw):
|
||||
extraData['trackAPI'] = trackAPI_gw['_EXTRA_TRACK'].copy()
|
||||
del extraData['trackAPI_gw']['_EXTRA_TRACK']
|
||||
del trackAPI_gw['_EXTRA_TRACK']
|
||||
# Temp metadata to generate logs
|
||||
tempTrack = {
|
||||
'id': trackAPI_gw['SNG_ID'],
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
class Plugin:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def setup(self):
|
||||
pass
|
||||
|
||||
def parseLink(self, link):
|
||||
pass
|
||||
|
||||
def generateDownloadObject(self, dz, link, bitrate, listener):
|
||||
pass
|
|
@ -0,0 +1,346 @@
|
|||
from concurrent.futures import ThreadPoolExecutor
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
from urllib.request import urlopen
|
||||
from deemix.plugins import Plugin
|
||||
from deemix.utils.localpaths import getConfigFolder
|
||||
from deemix.itemgen import generateTrackItem, generateAlbumItem, GenerationError, TrackNotOnDeezer, AlbumNotOnDeezer
|
||||
from deemix.types.DownloadObjects import Convertable
|
||||
|
||||
import spotipy
|
||||
SpotifyClientCredentials = spotipy.oauth2.SpotifyClientCredentials
|
||||
|
||||
class Spotify(Plugin):
|
||||
def __init__(self, configFolder=None):
|
||||
super().__init__()
|
||||
self.credentials = {'clientId': "", 'clientSecret': ""}
|
||||
self.settings = {
|
||||
'fallbackSearch': False
|
||||
}
|
||||
self.enabled = False
|
||||
self.sp = None
|
||||
self.configFolder = Path(configFolder or getConfigFolder())
|
||||
self.configFolder /= 'spotify'
|
||||
|
||||
def setup(self):
|
||||
if not self.configFolder.is_dir(): self.configFolder.mkdir()
|
||||
|
||||
self.loadSettings()
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def parseLink(cls, link):
|
||||
if 'link.tospotify.com' in link: link = urlopen(link).url
|
||||
# Remove extra stuff
|
||||
if '?' in link: link = link[:link.find('?')]
|
||||
if '&' in link: link = link[:link.find('&')]
|
||||
if link.endswith('/'): link = link[:-1] # Remove last slash if present
|
||||
|
||||
link_type = None
|
||||
link_id = None
|
||||
|
||||
if not 'spotify' in link: return (link, link_type, link_id) # return if not a spotify link
|
||||
|
||||
if re.search(r"[/:]track[/:](.+)", link):
|
||||
link_type = 'track'
|
||||
link_id = re.search(r"[/:]track[/:](.+)", link).group(1)
|
||||
elif re.search(r"[/:]album[/:](.+)", link):
|
||||
link_type = 'album'
|
||||
link_id = re.search(r"[/:]album[/:](.+)", link).group(1)
|
||||
elif re.search(r"[/:]playlist[/:](.+)", link):
|
||||
link_type = 'playlist'
|
||||
link_id = re.search(r"[/:]playlist[/:](.+)", link).group(1)
|
||||
|
||||
return (link, link_type, link_id)
|
||||
|
||||
def generateDownloadObject(self, dz, link, bitrate):
|
||||
(link, link_type, link_id) = self.parseLink(link)
|
||||
|
||||
if link_type is None or link_id is None: return None
|
||||
|
||||
if link_type == "track":
|
||||
return self.generateTrackItem(dz, link_id, bitrate)
|
||||
if link_type == "album":
|
||||
return self.generateAlbumItem(dz, link_id, bitrate)
|
||||
if link_type == "playlist":
|
||||
return self.generatePlaylistItem(dz, link_id, bitrate)
|
||||
return None
|
||||
|
||||
def generateTrackItem(self, dz, link_id, bitrate):
|
||||
cache = self.loadCache()
|
||||
|
||||
if link_id in cache['tracks']:
|
||||
cachedTrack = cache['tracks'][link_id]
|
||||
else:
|
||||
cachedTrack = self.getTrack(link_id)
|
||||
cache['tracks'][link_id] = cachedTrack
|
||||
self.saveCache(cache)
|
||||
|
||||
if 'isrc' in cachedTrack:
|
||||
try: return generateTrackItem(dz, f"isrc:{cachedTrack['isrc']}", bitrate)
|
||||
except GenerationError: pass
|
||||
if self.settings['fallbackSearch']:
|
||||
if 'id' not in cachedTrack or cachedTrack['id'] == "0":
|
||||
trackID = dz.api.get_track_id_from_metadata(
|
||||
cachedTrack['data']['artist'],
|
||||
cachedTrack['data']['title'],
|
||||
cachedTrack['data']['album'],
|
||||
)
|
||||
if trackID != "0":
|
||||
cachedTrack['id'] = trackID
|
||||
cache['tracks'][link_id] = cachedTrack
|
||||
self.saveCache(cache)
|
||||
if cachedTrack['id'] != "0": return generateTrackItem(dz, cachedTrack['id'], bitrate)
|
||||
raise TrackNotOnDeezer(f"https://open.spotify.com/track/{link_id}")
|
||||
|
||||
def generateAlbumItem(self, dz, link_id, bitrate):
|
||||
cache = self.loadCache()
|
||||
|
||||
if link_id in cache['albums']:
|
||||
cachedAlbum = cache['albums'][link_id]
|
||||
else:
|
||||
cachedAlbum = self.getAlbum(link_id)
|
||||
cache['albums'][link_id] = cachedAlbum
|
||||
self.saveCache(cache)
|
||||
|
||||
try: return generateAlbumItem(dz, f"upc:{cachedAlbum['upc']}", bitrate)
|
||||
except GenerationError as e: raise AlbumNotOnDeezer(f"https://open.spotify.com/album/{link_id}") from e
|
||||
|
||||
def generatePlaylistItem(self, dz, link_id, bitrate):
|
||||
if not self.enabled: raise Exception("Spotify plugin not enabled")
|
||||
spotifyPlaylist = self.sp.playlist(link_id)
|
||||
|
||||
playlistAPI = self._convertPlaylistStructure(spotifyPlaylist)
|
||||
playlistAPI.various_artist = dz.api.get_artist(5080) # Useful for save as compilation
|
||||
|
||||
tracklistTemp = spotifyPlaylist.track.items
|
||||
while spotifyPlaylist['tracks']['next']:
|
||||
spotifyPlaylist['tracks'] = self.sp.next(spotifyPlaylist['tracks'])
|
||||
tracklistTemp += spotifyPlaylist['tracks']['items']
|
||||
|
||||
tracklist = []
|
||||
for item in tracklistTemp:
|
||||
if item['track']:
|
||||
if item['track']['explicit']:
|
||||
playlistAPI['explicit'] = True
|
||||
tracklist.append(item['track'])
|
||||
if 'explicit' not in playlistAPI: playlistAPI['explicit'] = False
|
||||
|
||||
return Convertable({
|
||||
'type': 'spotify_playlist',
|
||||
'id': link_id,
|
||||
'bitrate': bitrate,
|
||||
'title': spotifyPlaylist['name'],
|
||||
'artist': spotifyPlaylist['owner']['display_name'],
|
||||
'cover': playlistAPI['picture_thumbnail'],
|
||||
'explicit': playlistAPI['explicit'],
|
||||
'size': len(tracklist),
|
||||
'collection': {
|
||||
'tracks_gw': [],
|
||||
'playlistAPI': playlistAPI
|
||||
},
|
||||
'plugin': 'spotify',
|
||||
'conversion_data': tracklist
|
||||
})
|
||||
|
||||
def getTrack(self, track_id, spotifyTrack=None):
|
||||
if not self.enabled: raise Exception("Spotify plugin not enabled")
|
||||
cachedTrack = {
|
||||
'isrc': None,
|
||||
'data': None
|
||||
}
|
||||
|
||||
if not spotifyTrack:
|
||||
spotifyTrack = self.sp.track(track_id)
|
||||
if 'isrc' in spotifyTrack.get('external_ids', {}):
|
||||
cachedTrack['isrc'] = spotifyTrack['external_ids']['isrc']
|
||||
cachedTrack['data'] = {
|
||||
'title': spotifyTrack['name'],
|
||||
'artist': spotifyTrack['artists'][0]['name'],
|
||||
'album': spotifyTrack['album']['name']
|
||||
}
|
||||
return cachedTrack
|
||||
|
||||
def getAlbum(self, album_id, spotifyAlbum=None):
|
||||
if not self.enabled: raise Exception("Spotify plugin not enabled")
|
||||
cachedAlbum = {
|
||||
'upc': None,
|
||||
'data': None
|
||||
}
|
||||
|
||||
if not spotifyAlbum:
|
||||
spotifyAlbum = self.sp.album(album_id)
|
||||
if 'upc' in spotifyAlbum.get('external_ids', {}):
|
||||
cachedAlbum['upc'] = spotifyAlbum['external_ids']['upc']
|
||||
cachedAlbum['data'] = {
|
||||
'title': spotifyAlbum['name'],
|
||||
'artist': spotifyAlbum['artists'][0]['name']
|
||||
}
|
||||
return cachedAlbum
|
||||
|
||||
def convertTrack(self, dz, downloadObject, track, pos, conversion, conversionNext, cache, listener):
|
||||
if downloadObject.isCanceled: return
|
||||
|
||||
if track['id'] in cache['tracks']:
|
||||
cachedTrack = cache['tracks'][track['id']]
|
||||
else:
|
||||
cachedTrack = self.getTrack(track['id'], track)
|
||||
cache['tracks'][track['id']] = cachedTrack
|
||||
self.saveCache(cache)
|
||||
|
||||
if 'isrc' in cachedTrack:
|
||||
try:
|
||||
trackAPI = dz.api.get_track_by_ISRC(cachedTrack['isrc'])
|
||||
if 'id' not in trackAPI or 'title' not in trackAPI: trackAPI = None
|
||||
except GenerationError: pass
|
||||
if self.settings['fallbackSearch'] and not trackAPI:
|
||||
if 'id' not in cachedTrack or cachedTrack['id'] == "0":
|
||||
trackID = dz.api.get_track_id_from_metadata(
|
||||
cachedTrack['data']['artist'],
|
||||
cachedTrack['data']['title'],
|
||||
cachedTrack['data']['album'],
|
||||
)
|
||||
if trackID != "0":
|
||||
cachedTrack['id'] = trackID
|
||||
cache['tracks'][track['id']] = cachedTrack
|
||||
self.saveCache(cache)
|
||||
if cachedTrack['id'] != "0": trackAPI = dz.api.get_track(cachedTrack['id'])
|
||||
|
||||
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']
|
||||
}
|
||||
else:
|
||||
deezerTrack = dz.gw.get_track_with_fallback(trackAPI['id'])
|
||||
deezerTrack['_EXTRA_TRACK'] = trackAPI
|
||||
deezerTrack['POSITION'] = pos+1
|
||||
|
||||
conversionNext += (1 / downloadObject.size) * 100
|
||||
if round(conversionNext) != conversion and round(conversionNext) % 2 == 0:
|
||||
conversion = round(conversionNext)
|
||||
if listener: listener.send("updateQueue", {'uuid': downloadObject.uuid, 'conversion': conversion})
|
||||
|
||||
def convert(self, dz, downloadObject, settings, listener=None):
|
||||
cache = self.loadCache()
|
||||
|
||||
conversion = 0
|
||||
conversionNext = 0
|
||||
|
||||
collection = [None] * len(downloadObject.conversion_data)
|
||||
with ThreadPoolExecutor(settings['queueConcurrency']) as executor:
|
||||
for pos, track in enumerate(downloadObject.conversion_data, start=0):
|
||||
collection[pos] = executor.submit(self.convertTrack,
|
||||
dz, downloadObject,
|
||||
track, pos,
|
||||
conversion, conversionNext,
|
||||
cache, listener
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _convertPlaylistStructure(cls, spotifyPlaylist):
|
||||
cover = None
|
||||
if len(spotifyPlaylist['images']): cover = spotifyPlaylist['images'][0]['url']
|
||||
|
||||
deezerPlaylist = {
|
||||
'checksum': spotifyPlaylist['snapshot_id'],
|
||||
'collaborative': spotifyPlaylist['collaborative'],
|
||||
'creation_date': "XXXX-00-00",
|
||||
'creator': {
|
||||
'id': spotifyPlaylist['owner']['id'],
|
||||
'name': spotifyPlaylist['owner']['display_name'],
|
||||
'tracklist': spotifyPlaylist['owner']['href'],
|
||||
'type': "user"
|
||||
},
|
||||
'description': spotifyPlaylist['description'],
|
||||
'duration': 0,
|
||||
'fans': spotifyPlaylist['followers']['total'] if 'followers' in spotifyPlaylist else 0,
|
||||
'id': spotifyPlaylist['id'],
|
||||
'is_loved_track': False,
|
||||
'link': spotifyPlaylist['external_urls']['spotify'],
|
||||
'nb_tracks': spotifyPlaylist['tracks']['total'],
|
||||
'picture': cover,
|
||||
'picture_small': cover or "https://e-cdns-images.dzcdn.net/images/cover/d41d8cd98f00b204e9800998ecf8427e/56x56-000000-80-0-0.jpg",
|
||||
'picture_medium': cover or "https://e-cdns-images.dzcdn.net/images/cover/d41d8cd98f00b204e9800998ecf8427e/250x250-000000-80-0-0.jpg",
|
||||
'picture_big': cover or "https://e-cdns-images.dzcdn.net/images/cover/d41d8cd98f00b204e9800998ecf8427e/500x500-000000-80-0-0.jpg",
|
||||
'picture_xl': cover or "https://e-cdns-images.dzcdn.net/images/cover/d41d8cd98f00b204e9800998ecf8427e/1000x1000-000000-80-0-0.jpg",
|
||||
'public': spotifyPlaylist['public'],
|
||||
'share': spotifyPlaylist['external_urls']['spotify'],
|
||||
'title': spotifyPlaylist['name'],
|
||||
'tracklist': spotifyPlaylist['tracks']['href'],
|
||||
'type': "playlist"
|
||||
}
|
||||
return deezerPlaylist
|
||||
|
||||
def loadSettings(self):
|
||||
if not (self.configFolder / 'settings.json').is_file():
|
||||
with open(self.configFolder / 'settings.json', 'w') as f:
|
||||
json.dump({**self.credentials, **self.settings}, f, indent=2)
|
||||
|
||||
with open(self.configFolder / 'settings.json', 'r') as settingsFile:
|
||||
settings = json.load(settingsFile)
|
||||
self.setSettings(settings)
|
||||
self.checkCredentials()
|
||||
|
||||
def saveSettings(self, newSettings=None):
|
||||
if newSettings: self.setSettings(newSettings)
|
||||
self.checkCredentials()
|
||||
with open(self.configFolder / 'settings.json', 'w') as f:
|
||||
json.dump({**self.credentials, **self.settings}, f, indent=2)
|
||||
|
||||
def getSettings(self):
|
||||
return {**self.credentials, **self.settings}
|
||||
|
||||
def setSettings(self, newSettings):
|
||||
self.credentials = { 'clientId': newSettings['clientId'], 'clientSecret': newSettings['clientSecret'] }
|
||||
settings = {**newSettings}
|
||||
del settings['clientId']
|
||||
del settings['clientSecret']
|
||||
self.settings = settings
|
||||
|
||||
def loadCache(self):
|
||||
if (self.configFolder / 'cache.json').is_file():
|
||||
with open(self.configFolder / 'cache.json', 'r') as f:
|
||||
cache = json.load(f)
|
||||
else:
|
||||
cache = {'tracks': {}, 'albums': {}}
|
||||
return cache
|
||||
|
||||
def saveCache(self, newCache):
|
||||
with open(self.configFolder / 'cache.json', 'w') as spotifyCache:
|
||||
json.dump(newCache, spotifyCache)
|
||||
|
||||
def checkCredentials(self):
|
||||
if self.credentials['clientId'] == "" or self.credentials['clientSecret'] == "":
|
||||
self.enabled = False
|
||||
return
|
||||
|
||||
try:
|
||||
client_credentials_manager = SpotifyClientCredentials(client_id=self.credentials['clientId'],
|
||||
client_secret=self.credentials['clientSecret'])
|
||||
self.sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
|
||||
self.sp.user_playlists('spotify')
|
||||
self.enabled = True
|
||||
except Exception:
|
||||
self.enabled = False
|
||||
|
||||
def getCredentials(self):
|
||||
return self.credentials
|
||||
|
||||
def setCredentials(self, clientId, clientSecret):
|
||||
# Remove extra spaces, just to be sure
|
||||
clientId = clientId.strip()
|
||||
clientSecret = clientSecret.strip()
|
||||
|
||||
# Save them to disk
|
||||
self.credentials = { 'clientId': clientId, 'clientSecret': clientSecret}
|
||||
self.saveSettings()
|
Loading…
Reference in New Issue