Browse Source

Merge refactoring (#4)

Removed saveDownloadQueue and tagsLanguage from lib settings

Revert embedded cover change

Fixed bitrate fallback check

Use overwriteFile setting when downloading embedded covers

Fixed bitrate fallback not working

Fixed some issues to make the lib work

Implemented spotify plugin back

Better handling of albums upcs

Fixed queue item not cancelling correctly

Code parity with deemix-js

Code cleanup with pylint

Even more rework on the library

More work on the library (WIP)

Total rework of the library (WIP)

Some rework done on types

Added start queue function

Made nextitem work on a thread

Removed dz as first parameter

Started queuemanager refactoring

Removed eventlet

Co-authored-by: RemixDev <RemixDev64@gmail.com>
Reviewed-on: https://git.freezer.life/RemixDev/deemix-py/pulls/4
Co-Authored-By: RemixDev <remixdev@noreply.localhost>
Co-Committed-By: RemixDev <remixdev@noreply.localhost>
pull/5/head
RemixDev 3 months ago
parent
commit
f530a4e89f
37 changed files with 2237 additions and 2464 deletions
  1. +1
    -0
      .gitignore
  2. +2
    -0
      .pylintrc
  3. +75
    -4
      deemix/__init__.py
  4. +49
    -10
      deemix/__main__.py
  5. +0
    -12
      deemix/app/__init__.py
  6. +0
    -40
      deemix/app/cli.py
  7. +0
    -767
      deemix/app/downloadjob.py
  8. +0
    -4
      deemix/app/messageinterface.py
  9. +0
    -115
      deemix/app/queueitem.py
  10. +0
    -569
      deemix/app/queuemanager.py
  11. +0
    -220
      deemix/app/settings.py
  12. +0
    -349
      deemix/app/spotifyhelper.py
  13. +156
    -0
      deemix/decryption.py
  14. +564
    -0
      deemix/downloader.py
  15. +307
    -0
      deemix/itemgen.py
  16. +12
    -0
      deemix/plugins/__init__.py
  17. +346
    -0
      deemix/plugins/spotify.py
  18. +137
    -0
      deemix/settings.py
  19. +5
    -5
      deemix/tagger.py
  20. +50
    -33
      deemix/types/Album.py
  21. +5
    -5
      deemix/types/Artist.py
  22. +4
    -4
      deemix/types/Date.py
  23. +126
    -0
      deemix/types/DownloadObjects.py
  24. +7
    -9
      deemix/types/Lyrics.py
  25. +26
    -24
      deemix/types/Picture.py
  26. +17
    -19
      deemix/types/Playlist.py
  27. +120
    -48
      deemix/types/Track.py
  28. +1
    -7
      deemix/types/__init__.py
  29. +24
    -88
      deemix/utils/__init__.py
  30. +26
    -0
      deemix/utils/crypto.py
  31. +0
    -31
      deemix/utils/decryption.py
  32. +32
    -0
      deemix/utils/deezer.py
  33. +58
    -30
      deemix/utils/localpaths.py
  34. +81
    -63
      deemix/utils/pathtemplates.py
  35. +0
    -1
      requirements.txt
  36. +4
    -5
      setup.py
  37. +2
    -2
      updatePyPi.sh

+ 1
- 0
.gitignore View File

@ -33,3 +33,4 @@ yarn-error.log*
/build
/*egg-info
updatePyPi.sh
/deezer

+ 2
- 0
.pylintrc View File

@ -0,0 +1,2 @@
[MESSAGES CONTROL]
disable=C0301,C0103,R0902,R0903,C0321,R0911,R0912,R0913,R0914,R0915,R0916

+ 75
- 4
deemix/__init__.py View File

@ -1,6 +1,77 @@
#!/usr/bin/env python3
import re
from urllib.request import urlopen
__version__ = "2.0.16"
USER_AGENT_HEADER = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) " \
"Chrome/79.0.3945.130 Safari/537.36"
VARIOUS_ARTISTS = "5080"
from deemix.itemgen import generateTrackItem, \
generateAlbumItem, \
generatePlaylistItem, \
generateArtistItem, \
generateArtistDiscographyItem, \
generateArtistTopItem, \
LinkNotRecognized, \
LinkNotSupported
__version__ = "3.0.0"
# Returns the Resolved URL, the Type and the ID
def parseLink(link):
if 'deezer.page.link' in link: link = urlopen(link).url # Resolve URL shortner
# 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 'deezer' in link: return (link, link_type, link_id) # return if not a deezer link
if '/track' in link:
link_type = 'track'
link_id = re.search(r"/track/(.+)", link).group(1)
elif '/playlist' in link:
link_type = 'playlist'
link_id = re.search(r"/playlist/(\d+)", link).group(1)
elif '/album' in link:
link_type = 'album'
link_id = re.search(r"/album/(.+)", link).group(1)
elif re.search(r"/artist/(\d+)/top_track", link):
link_type = 'artist_top'
link_id = re.search(r"/artist/(\d+)/top_track", link).group(1)
elif re.search(r"/artist/(\d+)/discography", link):
link_type = 'artist_discography'
link_id = re.search(r"/artist/(\d+)/discography", link).group(1)
elif '/artist' in link:
link_type = 'artist'
link_id = re.search(r"/artist/(\d+)", link).group(1)
return (link, link_type, link_id)
def generateDownloadObject(dz, link, bitrate, plugins=None, listener=None):
(link, link_type, link_id) = parseLink(link)
if link_type is None or link_id is None:
if plugins is None: plugins = {}
plugin_names = plugins.keys()
current_plugin = None
item = None
for plugin in plugin_names:
current_plugin = plugins[plugin]
item = current_plugin.generateDownloadObject(dz, link, bitrate, listener)
if item: return item
raise LinkNotRecognized(link)
if link_type == "track":
return generateTrackItem(dz, link_id, bitrate)
if link_type == "album":
return generateAlbumItem(dz, link_id, bitrate)
if link_type == "playlist":
return generatePlaylistItem(dz, link_id, bitrate)
if link_type == "artist":
return generateArtistItem(dz, link_id, bitrate, listener)
if link_type == "artist_discography":
return generateArtistDiscographyItem(dz, link_id, bitrate, listener)
if link_type == "artist_top":
return generateArtistTopItem(dz, link_id, bitrate)
raise LinkNotSupported(link)

+ 49
- 10
deemix/__main__.py View File

@ -1,37 +1,76 @@
#!/usr/bin/env python3
import click
from deemix.app.cli import cli
from pathlib import Path
from deezer import Deezer
from deezer import TrackFormats
from deemix import generateDownloadObject
from deemix.settings import load as loadSettings
from deemix.utils import getBitrateNumberFromText
import deemix.utils.localpaths as localpaths
from deemix.downloader import Downloader
@click.command()
@click.option('--portable', is_flag=True, help='Creates the config folder in the same directory where the script is launched')
@click.option('-b', '--bitrate', default=None, help='Overwrites the default bitrate selected')
@click.option('-p', '--path', type=str, help='Downloads in the given folder')
@click.argument('url', nargs=-1, required=True)
def download(url, bitrate, portable, path):
# Check for local configFolder
localpath = Path('.')
configFolder = localpath / 'config' if portable else None
configFolder = localpath / 'config' if portable else localpaths.getConfigFolder()
settings = loadSettings(configFolder)
dz = Deezer(settings.get('tagsLanguage', ""))
def requestValidArl():
while True:
arl = input("Paste here your arl:")
if dz.login_via_arl(arl.strip()): break
return arl
if (configFolder / '.arl').is_file():
with open(configFolder / '.arl', 'r') as f:
arl = f.readline().rstrip("\n").strip()
if not dz.login_via_arl(arl): arl = requestValidArl()
else: arl = requestValidArl()
with open(configFolder / '.arl', 'w') as f:
f.write(arl)
def downloadLinks(url, bitrate=None):
if not bitrate: bitrate = settings.get("maxBitrate", TrackFormats.MP3_320)
links = []
for link in url:
if ';' in link:
for l in link.split(";"):
links.append(l)
else:
links.append(link)
for link in links:
downloadObject = generateDownloadObject(dz, link, bitrate)
Downloader(dz, downloadObject, settings).start()
if path is not None:
if path == '': path = '.'
path = Path(path)
app = cli(path, configFolder)
app.login()
settings['downloadLocation'] = str(path)
url = list(url)
if bitrate: bitrate = getBitrateNumberFromText(bitrate)
# If first url is filepath readfile and use them as URLs
try:
isfile = Path(url[0]).is_file()
except:
except Exception:
isfile = False
if isfile:
filename = url[0]
with open(filename) as f:
url = f.readlines()
app.downloadLink(url, bitrate)
downloadLinks(url, bitrate)
click.echo("All done!")
if __name__ == '__main__':
download()
download() # pylint: disable=E1120

+ 0
- 12
deemix/app/__init__.py View File

@ -1,12 +0,0 @@
from deezer import Deezer
from deemix.app.settings import Settings
from deemix.app.queuemanager import QueueManager
from deemix.app.spotifyhelper import SpotifyHelper
class deemix:
def __init__(self, configFolder=None, overwriteDownloadFolder=None):
self.set = Settings(configFolder, overwriteDownloadFolder=overwriteDownloadFolder)
self.dz = Deezer()
self.dz.set_accept_language(self.set.settings.get('tagsLanguage'))
self.sp = SpotifyHelper(configFolder)
self.qm = QueueManager(self.sp)

+ 0
- 40
deemix/app/cli.py View File

@ -1,40 +0,0 @@
from pathlib import Path
from os import makedirs
from deemix.app import deemix
from deemix.utils import checkFolder
class cli(deemix):
def __init__(self, downloadpath, configFolder=None):
super().__init__(configFolder, overwriteDownloadFolder=downloadpath)
if downloadpath:
print("Using folder: "+self.set.settings['downloadLocation'])
def downloadLink(self, url, bitrate=None):
for link in url:
if ';' in link:
for l in link.split(";"):
self.qm.addToQueue(self.dz, l, self.set.settings, bitrate)
else:
self.qm.addToQueue(self.dz, link, self.set.settings, bitrate)
def requestValidArl(self):
while True:
arl = input("Paste here your arl:")
if self.dz.login_via_arl(arl):
break
return arl
def login(self):
configFolder = Path(self.set.configFolder)
if not configFolder.is_dir():
makedirs(configFolder, exist_ok=True)
if (configFolder / '.arl').is_file():
with open(configFolder / '.arl', 'r') as f:
arl = f.readline().rstrip("\n")
if not self.dz.login_via_arl(arl):
arl = self.requestValidArl()
else:
arl = self.requestValidArl()
with open(configFolder / '.arl', 'w') as f:
f.write(arl)

+ 0
- 767
deemix/app/downloadjob.py View File

@ -1,767 +0,0 @@
import eventlet
from eventlet.green.subprocess import call as execute
requests = eventlet.import_patched('requests')
get = requests.get
request_exception = requests.exceptions
from os.path import sep as pathSep
from pathlib import Path
from shlex import quote
import re
import errno
from ssl import SSLError
from os import makedirs
from tempfile import gettempdir
from urllib3.exceptions import SSLError as u3SSLError
from deemix.app.queueitem import QISingle, QICollection
from deemix.types.Track import Track, AlbumDoesntExists
from deemix.utils import changeCase
from deemix.utils.pathtemplates import generateFilename, generateFilepath, settingsRegexAlbum, settingsRegexArtist, settingsRegexPlaylistFile
from deezer import TrackFormats
from deemix import USER_AGENT_HEADER
from deemix.utils.taggers import tagID3, tagFLAC
from deemix.utils.decryption import generateStreamURL, generateBlowfishKey
from deemix.app.settings import OverwriteOption, FeaturesOption
from Cryptodome.Cipher import Blowfish
from mutagen.flac import FLACNoHeaderError, error as FLACError
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger('deemix')
TEMPDIR = Path(gettempdir()) / 'deemix-imgs'
if not TEMPDIR.is_dir(): makedirs(TEMPDIR)
extensions = {
TrackFormats.FLAC: '.flac',
TrackFormats.LOCAL: '.mp3',
TrackFormats.MP3_320: '.mp3',
TrackFormats.MP3_128: '.mp3',
TrackFormats.DEFAULT: '.mp3',
TrackFormats.MP4_RA3: '.mp4',
TrackFormats.MP4_RA2: '.mp4',
TrackFormats.MP4_RA1: '.mp4'
}
errorMessages = {
'notOnDeezer': "Track not available on Deezer!",
'notEncoded': "Track not yet encoded!",
'notEncodedNoAlternative': "Track not yet encoded and no alternative found!",
'wrongBitrate': "Track not found at desired bitrate.",
'wrongBitrateNoAlternative': "Track not found at desired bitrate and no alternative found!",
'no360RA': "Track is not available in Reality Audio 360.",
'notAvailable': "Track not available on deezer's servers!",
'notAvailableNoAlternative': "Track not available on deezer's servers and no alternative found!",
'noSpaceLeft': "No space left on target drive, clean up some space for the tracks",
'albumDoesntExists': "Track's album does not exsist, failed to gather info"
}
def downloadImage(url, path, overwrite=OverwriteOption.DONT_OVERWRITE):
if not path.is_file() or overwrite in [OverwriteOption.OVERWRITE, OverwriteOption.ONLY_TAGS, OverwriteOption.KEEP_BOTH]:
try:
image = get(url, headers={'User-Agent': USER_AGENT_HEADER}, timeout=30)
image.raise_for_status()
with open(path, 'wb') as f:
f.write(image.content)
return path
except request_exception.HTTPError:
if 'cdns-images.dzcdn.net' in url:
urlBase = url[:url.rfind("/")+1]
pictureUrl = url[len(urlBase):]
pictureSize = int(pictureUrl[:pictureUrl.find("x")])
if pictureSize > 1200:
logger.warn("Couldn't download "+str(pictureSize)+"x"+str(pictureSize)+" image, falling back to 1200x1200")
eventlet.sleep(1)
return downloadImage(urlBase+pictureUrl.replace(str(pictureSize)+"x"+str(pictureSize), '1200x1200'), path, overwrite)
logger.error("Image not found: "+url)
except (request_exception.ConnectionError, request_exception.ChunkedEncodingError, u3SSLError) as e:
logger.error("Couldn't download Image, retrying in 5 seconds...: "+url+"\n")
eventlet.sleep(5)
return downloadImage(url, path, overwrite)
except OSError as e:
if e.errno == errno.ENOSPC:
raise DownloadFailed("noSpaceLeft")
else:
logger.exception(f"Error while downloading an image, you should report this to the developers: {str(e)}")
except Exception as e:
logger.exception(f"Error while downloading an image, you should report this to the developers: {str(e)}")
if path.is_file(): path.unlink()
return None
else:
return path
class DownloadJob:
def __init__(self, dz, queueItem, interface=None):
self.dz = dz
self.interface = interface
self.queueItem = queueItem
self.settings = queueItem.settings
self.bitrate = queueItem.bitrate
self.downloadPercentage = 0
self.lastPercentage = 0
self.extrasPath = None
self.playlistCoverName = None
self.playlistURLs = []
def start(self):
if not self.queueItem.cancel:
if isinstance(self.queueItem, QISingle):
result = self.downloadWrapper(self.queueItem.single)
if result: self.singleAfterDownload(result)
elif isinstance(self.queueItem, QICollection):
tracks = [None] * len(self.queueItem.collection)
pool = eventlet.GreenPool(size=self.settings['queueConcurrency'])
for pos, track in enumerate(self.queueItem.collection, start=0):
tracks[pos] = pool.spawn(self.downloadWrapper, track)
pool.waitall()
self.collectionAfterDownload(tracks)
if self.interface:
if self.queueItem.cancel:
self.interface.send('currentItemCancelled', self.queueItem.uuid)
self.interface.send("removedFromQueue", self.queueItem.uuid)
else:
self.interface.send("finishDownload", self.queueItem.uuid)
return self.extrasPath
def singleAfterDownload(self, result):
if not self.extrasPath: self.extrasPath = Path(self.settings['downloadLocation'])
# Save Album Cover
if self.settings['saveArtwork'] and 'albumPath' in result:
for image in result['albumURLs']:
downloadImage(image['url'], result['albumPath'] / f"{result['albumFilename']}.{image['ext']}", self.settings['overwriteFile'])
# Save Artist Artwork
if self.settings['saveArtworkArtist'] and 'artistPath' in result:
for image in result['artistURLs']:
downloadImage(image['url'], result['artistPath'] / f"{result['artistFilename']}.{image['ext']}", self.settings['overwriteFile'])
# Create searched logfile
if self.settings['logSearched'] and 'searched' in result:
with open(self.extrasPath / 'searched.txt', 'wb+') as f:
orig = f.read().decode('utf-8')
if not result['searched'] in orig:
if orig != "": orig += "\r\n"
orig += result['searched'] + "\r\n"
f.write(orig.encode('utf-8'))
# Execute command after download
if self.settings['executeCommand'] != "":
execute(self.settings['executeCommand'].replace("%folder%", quote(str(self.extrasPath))).replace("%filename%", quote(result['filename'])), shell=True)
def collectionAfterDownload(self, tracks):
if not self.extrasPath: self.extrasPath = Path(self.settings['downloadLocation'])
playlist = [None] * len(tracks)
errors = ""
searched = ""
for i in range(len(tracks)):
result = tracks[i].wait()
if not result: return None # Check if item is cancelled
# Log errors to file
if result.get('error'):
if not result['error'].get('data'): result['error']['data'] = {'id': "0", 'title': 'Unknown', 'artist': 'Unknown'}
errors += f"{result['error']['data']['id']} | {result['error']['data']['artist']} - {result['error']['data']['title']} | {result['error']['message']}\r\n"
# Log searched to file
if 'searched' in result: searched += result['searched'] + "\r\n"
# Save Album Cover
if self.settings['saveArtwork'] and 'albumPath' in result:
for image in result['albumURLs']:
downloadImage(image['url'], result['albumPath'] / f"{result['albumFilename']}.{image['ext']}", self.settings['overwriteFile'])
# Save Artist Artwork
if self.settings['saveArtworkArtist'] and 'artistPath' in result:
for image in result['artistURLs']:
downloadImage(image['url'], result['artistPath'] / f"{result['artistFilename']}.{image['ext']}", self.settings['overwriteFile'])
# Save filename for playlist file
playlist[i] = result.get('filename', "")
# Create errors logfile
if self.settings['logErrors'] and errors != "":
with open(self.extrasPath / 'errors.txt', 'wb') as f:
f.write(errors.encode('utf-8'))
# Create searched logfile
if self.settings['logSearched'] and searched != "":
with open(self.extrasPath / 'searched.txt', 'wb') as f:
f.write(searched.encode('utf-8'))
# Save Playlist Artwork
if self.settings['saveArtwork'] and self.playlistCoverName and not self.settings['tags']['savePlaylistAsCompilation']:
for image in self.playlistURLs:
downloadImage(image['url'], self.extrasPath / f"{self.playlistCoverName}.{image['ext']}", self.settings['overwriteFile'])
# Create M3U8 File
if self.settings['createM3U8File']:
filename = settingsRegexPlaylistFile(self.settings['playlistFilenameTemplate'], self.queueItem, self.settings) or "playlist"
with open(self.extrasPath / f'{filename}.m3u8', 'wb') as f:
for line in playlist:
f.write((line + "\n").encode('utf-8'))
# Execute command after download
if self.settings['executeCommand'] != "":
execute(self.settings['executeCommand'].replace("%folder%", quote(str(self.extrasPath))), shell=True)
def download(self, trackAPI_gw, track=None):
result = {}
if self.queueItem.cancel: raise DownloadCancelled
if trackAPI_gw['SNG_ID'] == "0": raise DownloadFailed("notOnDeezer")
# Create Track object
if not track:
logger.info(f"[{trackAPI_gw['ART_NAME']} - {trackAPI_gw['SNG_TITLE']}] Getting the tags")
try:
track = Track().parseData(
dz=self.dz,
trackAPI_gw=trackAPI_gw,
trackAPI=trackAPI_gw['_EXTRA_TRACK'] if '_EXTRA_TRACK' in trackAPI_gw else None,
albumAPI=trackAPI_gw['_EXTRA_ALBUM'] if '_EXTRA_ALBUM' in trackAPI_gw else None,
playlistAPI = trackAPI_gw['_EXTRA_PLAYLIST'] if '_EXTRA_PLAYLIST' in trackAPI_gw else None
)
except AlbumDoesntExists:
raise DownloadError('albumDoesntExists')
if self.queueItem.cancel: raise DownloadCancelled
# Check if track not yet encoded
if track.MD5 == '':
if track.fallbackId != "0":
logger.warn(f"[{track.mainArtist.name} - {track.title}] Track not yet encoded, using fallback id")
newTrack = self.dz.gw.get_track_with_fallback(track.fallbackId)
track.parseEssentialData(newTrack)
track.retriveFilesizes(self.dz)
return self.download(trackAPI_gw, track)
elif not track.searched and self.settings['fallbackSearch']:
logger.warn(f"[{track.mainArtist.name} - {track.title}] Track not yet encoded, searching for alternative")
searchedId = self.dz.api.get_track_id_from_metadata(track.mainArtist.name, track.title, track.album.title)
if searchedId != "0":
newTrack = self.dz.gw.get_track_with_fallback(searchedId)
track.parseEssentialData(newTrack)
track.retriveFilesizes(self.dz)
track.searched = True
if self.interface:
self.interface.send('queueUpdate', {
'uuid': self.queueItem.uuid,
'searchFallback': True,
'data': {
'id': track.id,
'title': track.title,
'artist': track.mainArtist.name
},
})
return self.download(trackAPI_gw, track)
else:
raise DownloadFailed("notEncodedNoAlternative")
else:
raise DownloadFailed("notEncoded")
# Choose the target bitrate
try:
selectedFormat = self.getPreferredBitrate(track)
except PreferredBitrateNotFound:
if track.fallbackId != "0":
logger.warn(f"[{track.mainArtist.name} - {track.title}] Track not found at desired bitrate, using fallback id")
newTrack = self.dz.gw.get_track_with_fallback(track.fallbackId)
track.parseEssentialData(newTrack)
track.retriveFilesizes(self.dz)
return self.download(trackAPI_gw, track)
elif not track.searched and self.settings['fallbackSearch']:
logger.warn(f"[{track.mainArtist.name} - {track.title}] Track not found at desired bitrate, searching for alternative")
searchedId = self.dz.api.get_track_id_from_metadata(track.mainArtist.name, track.title, track.album.title)
if searchedId != "0":
newTrack = self.dz.gw.get_track_with_fallback(searchedId)
track.parseEssentialData(newTrack)
track.retriveFilesizes(self.dz)
track.searched = True
if self.interface:
self.interface.send('queueUpdate', {
'uuid': self.queueItem.uuid,
'searchFallback': True,
'data': {
'id': track.id,
'title': track.title,
'artist': track.mainArtist.name
},
})
return self.download(trackAPI_gw, track)
else:
raise DownloadFailed("wrongBitrateNoAlternative")
else:
raise DownloadFailed("wrongBitrate")
except TrackNot360:
raise DownloadFailed("no360RA")
track.selectedFormat = selectedFormat
track.album.bitrate = selectedFormat
# Generate covers URLs
embeddedImageFormat = f'jpg-{self.settings["jpegImageQuality"]}'
if self.settings['embeddedArtworkPNG']: imageFormat = 'png'
if self.settings['tags']['savePlaylistAsCompilation'] and track.playlist:
track.trackNumber = track.position
track.discNumber = "1"
track.album.makePlaylistCompilation(track.playlist)
track.album.embeddedCoverURL = track.playlist.pic.generatePictureURL(self.settings['embeddedArtworkSize'], embeddedImageFormat)
ext = track.album.embeddedCoverURL[-4:]
if ext[0] != ".": ext = ".jpg" # Check for Spotify images
track.album.embeddedCoverPath = TEMPDIR / f"pl{trackAPI_gw['_EXTRA_PLAYLIST']['id']}_{self.settings['embeddedArtworkSize']}{ext}"
else:
if track.album.date: track.date = track.album.date
track.album.embeddedCoverURL = track.album.pic.generatePictureURL(self.settings['embeddedArtworkSize'], embeddedImageFormat)
ext = track.album.embeddedCoverURL[-4:]
track.album.embeddedCoverPath = TEMPDIR / f"alb{track.album.id}_{self.settings['embeddedArtworkSize']}{ext}"
track.dateString = track.date.format(self.settings['dateFormat'])
track.album.dateString = track.album.date.format(self.settings['dateFormat'])
if track.playlist: track.playlist.dateString = track.playlist.date.format(self.settings['dateFormat'])
# Check various artist option
if self.settings['albumVariousArtists'] and track.album.variousArtists:
artist = track.album.variousArtists
isMainArtist = artist.role == "Main"
if artist.name not in track.album.artists:
track.album.artists.insert(0, artist.name)
if isMainArtist or artist.name not in track.album.artist['Main'] and not isMainArtist:
if not artist.role in track.album.artist:
track.album.artist[artist.role] = []
track.album.artist[artist.role].insert(0, artist.name)
track.album.mainArtist.save = not track.album.mainArtist.isVariousArtists() or self.settings['albumVariousArtists'] and track.album.mainArtist.isVariousArtists()
# Check removeDuplicateArtists
if self.settings['removeDuplicateArtists']: track.removeDuplicateArtists()
# Check if user wants the feat in the title
if str(self.settings['featuredToTitle']) == FeaturesOption.REMOVE_TITLE:
track.title = track.getCleanTitle()
elif str(self.settings['featuredToTitle']) == FeaturesOption.MOVE_TITLE:
track.title = track.getFeatTitle()
elif str(self.settings['featuredToTitle']) == FeaturesOption.REMOVE_TITLE_ALBUM:
track.title = track.getCleanTitle()
track.album.title = track.album.getCleanTitle()
# Remove (Album Version) from tracks that have that
if self.settings['removeAlbumVersion']:
if "Album Version" in track.title:
track.title = re.sub(r' ?\(Album Version\)', "", track.title).strip()
# Change Title and Artists casing if needed
if self.settings['titleCasing'] != "nothing":
track.title = changeCase(track.title, self.settings['titleCasing'])
if self.settings['artistCasing'] != "nothing":
track.mainArtist.name = changeCase(track.mainArtist.name, self.settings['artistCasing'])
for i, artist in enumerate(track.artists):
track.artists[i] = changeCase(artist, self.settings['artistCasing'])
for type in track.artist:
for i, artist in enumerate(track.artist[type]):
track.artist[type][i] = changeCase(artist, self.settings['artistCasing'])
track.generateMainFeatStrings()
# Generate artist tag
if self.settings['tags']['multiArtistSeparator'] == "default":
if str(self.settings['featuredToTitle']) == FeaturesOption.MOVE_TITLE:
track.artistsString = ", ".join(track.artist['Main'])
else:
track.artistsString = ", ".join(track.artists)
elif self.settings['tags']['multiArtistSeparator'] == "andFeat":
track.artistsString = track.mainArtistsString
if track.featArtistsString and str(self.settings['featuredToTitle']) != FeaturesOption.MOVE_TITLE:
track.artistsString += " " + track.featArtistsString
else:
separator = self.settings['tags']['multiArtistSeparator']
if str(self.settings['featuredToTitle']) == FeaturesOption.MOVE_TITLE:
track.artistsString = separator.join(track.artist['Main'])
else:
track.artistsString = separator.join(track.artists)
# Generate filename and filepath from metadata
filename = generateFilename(track, self.settings, trackAPI_gw['FILENAME_TEMPLATE'])
(filepath, artistPath, coverPath, extrasPath) = generateFilepath(track, self.settings)
if self.queueItem.cancel: raise DownloadCancelled
# Download and cache coverart
logger.info(f"[{track.mainArtist.name} - {track.title}] Getting the album cover")
track.album.embeddedCoverPath = downloadImage(track.album.embeddedCoverURL, track.album.embeddedCoverPath)
# Save local album art
if coverPath:
result['albumURLs'] = []
for format in self.settings['localArtworkFormat'].split(","):
if format in ["png","jpg"]:
extendedFormat = format
if extendedFormat == "jpg": extendedFormat += f"-{self.settings['jpegImageQuality']}"
url = track.album.pic.generatePictureURL(self.settings['localArtworkSize'], extendedFormat)
if self.settings['tags']['savePlaylistAsCompilation'] \
and track.playlist \
and track.playlist.pic.url \
and not format.startswith("jpg"):
continue
result['albumURLs'].append({'url': url, 'ext': format})
result['albumPath'] = coverPath
result['albumFilename'] = f"{settingsRegexAlbum(self.settings['coverImageTemplate'], track.album, self.settings, track.playlist)}"
# Save artist art
if artistPath:
result['artistURLs'] = []
for format in self.settings['localArtworkFormat'].split(","):
if format in ["png","jpg"]:
extendedFormat = format
if extendedFormat == "jpg": extendedFormat += f"-{self.settings['jpegImageQuality']}"
url = track.album.mainArtist.pic.generatePictureURL(self.settings['localArtworkSize'], extendedFormat)
if track.album.mainArtist.pic.md5 == "" and not format.startswith("jpg"): continue
result['artistURLs'].append({'url': url, 'ext': format})
result['artistPath'] = artistPath
result['artistFilename'] = f"{settingsRegexArtist(self.settings['artistImageTemplate'], track.album.mainArtist, self.settings, rootArtist=track.album.rootArtist)}"
# Save playlist cover
if track.playlist:
if not len(self.playlistURLs):
for format in self.settings['localArtworkFormat'].split(","):
if format in ["png","jpg"]:
extendedFormat = format
if extendedFormat == "jpg": extendedFormat += f"-{self.settings['jpegImageQuality']}"
url = track.playlist.pic.generatePictureURL(self.settings['localArtworkSize'], extendedFormat)
if track.playlist.pic.url and not format.startswith("jpg"): continue
self.playlistURLs.append({'url': url, 'ext': format})
if not self.playlistCoverName:
track.playlist.bitrate = selectedFormat
track.playlist.dateString = track.playlist.date.format(self.settings['dateFormat'])
self.playlistCoverName = f"{settingsRegexAlbum(self.settings['coverImageTemplate'], track.playlist, self.settings, track.playlist)}"
# Remove subfolders from filename and add it to filepath
if pathSep in filename:
tempPath = filename[:filename.rfind(pathSep)]
filepath = filepath / tempPath
filename = filename[filename.rfind(pathSep) + len(pathSep):]
# Make sure the filepath exists
makedirs(filepath, exist_ok=True)
writepath = filepath / f"{filename}{extensions[track.selectedFormat]}"
# Save lyrics in lrc file
if self.settings['syncedLyrics'] and track.lyrics.sync:
if not (filepath / f"{filename}.lrc").is_file() or self.settings['overwriteFile'] in [OverwriteOption.OVERWRITE, OverwriteOption.ONLY_TAGS]:
with open(filepath / f"{filename}.lrc", 'wb') as f:
f.write(track.lyrics.sync.encode('utf-8'))
trackAlreadyDownloaded = writepath.is_file()
# Don't overwrite and don't mind extension
if not trackAlreadyDownloaded and self.settings['overwriteFile'] == OverwriteOption.DONT_CHECK_EXT:
exts = ['.mp3', '.flac', '.opus', '.m4a']
baseFilename = str(filepath / filename)
for ext in exts:
trackAlreadyDownloaded = Path(baseFilename+ext).is_file()
if trackAlreadyDownloaded: break
# Don't overwrite and keep both files
if trackAlreadyDownloaded and self.settings['overwriteFile'] == OverwriteOption.KEEP_BOTH:
baseFilename = str(filepath / filename)
i = 1
currentFilename = baseFilename+' ('+str(i)+')'+ extensions[track.selectedFormat]
while Path(currentFilename).is_file():
i += 1
currentFilename = baseFilename+' ('+str(i)+')'+ extensions[track.selectedFormat]
trackAlreadyDownloaded = False
writepath = Path(currentFilename)
if extrasPath:
if not self.extrasPath: self.extrasPath = extrasPath
result['filename'] = str(writepath)[len(str(extrasPath))+ len(pathSep):]
if not trackAlreadyDownloaded or self.settings['overwriteFile'] == OverwriteOption.OVERWRITE:
logger.info(f"[{track.mainArtist.name} - {track.title}] Downloading the track")
track.downloadUrl = generateStreamURL(track.id, track.MD5, track.mediaVersion, track.selectedFormat)
def downloadMusic(track, trackAPI_gw):
try:
with open(writepath, 'wb') as stream:
self.streamTrack(stream, track)
except DownloadCancelled:
if writepath.is_file(): writepath.unlink()
raise DownloadCancelled
except (request_exception.HTTPError, DownloadEmpty):
if writepath.is_file(): writepath.unlink()
if track.fallbackId != "0":
logger.warn(f"[{track.mainArtist.name} - {track.title}] Track not available, using fallback id")
newTrack = self.dz.gw.get_track_with_fallback(track.fallbackId)
track.parseEssentialData(newTrack)
track.retriveFilesizes(self.dz)
return False
elif not track.searched and self.settings['fallbackSearch']:
logger.warn(f"[{track.mainArtist.name} - {track.title}] Track not available, searching for alternative")
searchedId = self.dz.api.get_track_id_from_metadata(track.mainArtist.name, track.title, track.album.title)
if searchedId != "0":
newTrack = self.dz.gw.get_track_with_fallback(searchedId)
track.parseEssentialData(newTrack)
track.retriveFilesizes(self.dz)
track.searched = True
if self.interface:
self.interface.send('queueUpdate', {
'uuid': self.queueItem.uuid,
'searchFallback': True,
'data': {
'id': track.id,
'title': track.title,
'artist': track.mainArtist.name
},
})
return False
else:
raise DownloadFailed("notAvailableNoAlternative")
else:
raise DownloadFailed("notAvailable")
except (request_exception.ConnectionError, request_exception.ChunkedEncodingError) as e:
if writepath.is_file(): writepath.unlink()
logger.warn(f"[{track.mainArtist.name} - {track.title}] Error while downloading the track, trying again in 5s...")
eventlet.sleep(5)
return downloadMusic(track, trackAPI_gw)
except OSError as e:
if e.errno == errno.ENOSPC:
raise DownloadFailed("noSpaceLeft")
else:
if writepath.is_file(): writepath.unlink()
logger.exception(f"[{track.mainArtist.name} - {track.title}] Error while downloading the track, you should report this to the developers: {str(e)}")
raise e
except Exception as e:
if writepath.is_file(): writepath.unlink()
logger.exception(f"[{track.mainArtist.name} - {track.title}] Error while downloading the track, you should report this to the developers: {str(e)}")
raise e
return True
try:
trackDownloaded = downloadMusic(track, trackAPI_gw)
except Exception as e:
raise e
if not trackDownloaded: return self.download(trackAPI_gw, track)
else:
logger.info(f"[{track.mainArtist.name} - {track.title}] Skipping track as it's already downloaded")
self.completeTrackPercentage()
# Adding tags
if (not trackAlreadyDownloaded or self.settings['overwriteFile'] in [OverwriteOption.ONLY_TAGS, OverwriteOption.OVERWRITE]) and not track.localTrack:
logger.info(f"[{track.mainArtist.name} - {track.title}] Applying tags to the track")
if track.selectedFormat in [TrackFormats.MP3_320, TrackFormats.MP3_128, TrackFormats.DEFAULT]:
tagID3(writepath, track, self.settings['tags'])
elif track.selectedFormat == TrackFormats.FLAC:
try:
tagFLAC(writepath, track, self.settings['tags'])
except (FLACNoHeaderError, FLACError):
if writepath.is_file(): writepath.unlink()
logger.warn(f"[{track.mainArtist.name} - {track.title}] Track not available in FLAC, falling back if necessary")
self.removeTrackPercentage()
track.filesizes['FILESIZE_FLAC'] = "0"
track.filesizes['FILESIZE_FLAC_TESTED'] = True
return self.download(trackAPI_gw, track)
if track.searched: result['searched'] = f"{track.mainArtist.name} - {track.title}"
logger.info(f"[{track.mainArtist.name} - {track.title}] Track download completed\n{str(writepath)}")
self.queueItem.downloaded += 1
self.queueItem.files.append(str(writepath))
self.queueItem.extrasPath = str(self.extrasPath)
if self.interface:
self.interface.send("updateQueue", {'uuid': self.queueItem.uuid, 'downloaded': True, 'downloadPath': str(writepath), 'extrasPath': str(self.extrasPath)})
return result
def getPreferredBitrate(self, track):
if track.localTrack: return TrackFormats.LOCAL
shouldFallback = self.settings['fallbackBitrate']
falledBack = False
formats_non_360 = {
TrackFormats.FLAC: "FLAC",
TrackFormats.MP3_320: "MP3_320",
TrackFormats.MP3_128: "MP3_128",
}
formats_360 = {
TrackFormats.MP4_RA3: "MP4_RA3",
TrackFormats.MP4_RA2: "MP4_RA2",
TrackFormats.MP4_RA1: "MP4_RA1",
}
is360format = int(self.bitrate) in formats_360
if not shouldFallback:
formats = formats_360
formats.update(formats_non_360)
elif is360format:
formats = formats_360
else:
formats = formats_non_360
for formatNumber, formatName in formats.items():
if formatNumber <= int(self.bitrate):
if f"FILESIZE_{formatName}" in track.filesizes:
if int(track.filesizes[f"FILESIZE_{formatName}"]) != 0: return formatNumber
if not track.filesizes[f"FILESIZE_{formatName}_TESTED"]:
request = requests.head(
generateStreamURL(track.id, track.MD5, track.mediaVersion, formatNumber),
headers={'User-Agent': USER_AGENT_HEADER},
timeout=30
)
try:
request.raise_for_status()
return formatNumber
except request_exception.HTTPError: # if the format is not available, Deezer returns a 403 error
pass
if not shouldFallback:
raise PreferredBitrateNotFound
else:
if not falledBack:
falledBack = True
logger.info(f"[{track.mainArtist.name} - {track.title}] Fallback to lower bitrate")
if self.interface:
self.interface.send('queueUpdate', {
'uuid': self.queueItem.uuid,
'bitrateFallback': True,
'data': {
'id': track.id,
'title': track.title,
'artist': track.mainArtist.name
},
})
if is360format: raise TrackNot360
return TrackFormats.DEFAULT
def streamTrack(self, stream, track, start=0):
if self.queueItem.cancel: raise DownloadCancelled
headers=dict(self.dz.http_headers)
if range != 0: headers['Range'] = f'bytes={start}-'
chunkLength = start
percentage = 0
itemName = f"[{track.mainArtist.name} - {track.title}]"
try:
with self.dz.session.get(track.downloadUrl, headers=headers, stream=True, timeout=10) as request:
request.raise_for_status()
blowfish_key = str.encode(generateBlowfishKey(str(track.id)))
complete = int(request.headers["Content-Length"])
if complete == 0: raise DownloadEmpty
if start != 0:
responseRange = request.headers["Content-Range"]
logger.info(f'{itemName} downloading range {responseRange}')
else:
logger.info(f'{itemName} downloading {complete} bytes')
for chunk in request.iter_content(2048 * 3):
if self.queueItem.cancel: raise DownloadCancelled
if len(chunk) >= 2048:
chunk = Blowfish.new(blowfish_key, Blowfish.MODE_CBC, b"\x00\x01\x02\x03\x04\x05\x06\x07").decrypt(chunk[0:2048]) + chunk[2048:]
stream.write(chunk)
chunkLength += len(chunk)
if isinstance(self.queueItem, QISingle):
percentage = (chunkLength / (complete + start)) * 100
self.downloadPercentage = percentage
else:
chunkProgres = (len(chunk) / (complete + start)) / self.queueItem.size * 100
self.downloadPercentage += chunkProgres
self.updatePercentage()
except (SSLError, u3SSLError) as e:
logger.info(f'{itemName} retrying from byte {chunkLength}')
return self.streamTrack(stream, track, chunkLength)
except (request_exception.ConnectionError, requests.exceptions.ReadTimeout):
eventlet.sleep(2)
return self.streamTrack(stream, track, start)
def updatePercentage(self):
if round(self.downloadPercentage) != self.lastPercentage and round(self.downloadPercentage) % 2 == 0:
self.lastPercentage = round(self.downloadPercentage)
self.queueItem.progress = self.lastPercentage
if self.interface: self.interface.send("updateQueue", {'uuid': self.queueItem.uuid, 'progress': self.lastPercentage})
def completeTrackPercentage(self):
if isinstance(self.queueItem, QISingle):
self.downloadPercentage = 100
else:
self.downloadPercentage += (1 / self.queueItem.size) * 100
self.updatePercentage()
def removeTrackPercentage(self):
if isinstance(self.queueItem, QISingle):
self.downloadPercentage = 0
else:
self.downloadPercentage -= (1 / self.queueItem.size) * 100
self.updatePercentage()
def downloadWrapper(self, trackAPI_gw):
track = {
'id': trackAPI_gw['SNG_ID'],
'title': trackAPI_gw['SNG_TITLE'].strip(),
'artist': trackAPI_gw['ART_NAME']
}
if trackAPI_gw.get('VERSION') and trackAPI_gw['VERSION'] not in trackAPI_gw['SNG_TITLE']:
track['title'] += f" {trackAPI_gw['VERSION']}".strip()
try:
result = self.download(trackAPI_gw)
except DownloadCancelled:
return None
except DownloadFailed as error:
logger.error(f"[{track['artist']} - {track['title']}] {error.message}")
result = {'error': {
'message': error.message,
'errid': error.errid,
'data': track
}}
except Exception as e:
logger.exception(f"[{track['artist']} - {track['title']}] {str(e)}")
result = {'error': {
'message': str(e),
'data': track
}}
if 'error' in result:
self.completeTrackPercentage()
self.queueItem.failed += 1
self.queueItem.errors.append(result['error'])
if self.interface:
error = result['error']
self.interface.send("updateQueue", {
'uuid': self.queueItem.uuid,
'failed': True,
'data': error['data'],
'error': error['message'],
'errid': error['errid'] if 'errid' in error else None
})
return result
class DownloadError(Exception):
"""Base class for exceptions in this module."""
pass
class DownloadFailed(DownloadError):
def __init__(self, errid):
self.errid = errid
self.message = errorMessages[self.errid]
class DownloadCancelled(DownloadError):
pass
class DownloadEmpty(DownloadError):
pass
class PreferredBitrateNotFound(DownloadError):
pass
class TrackNot360(DownloadError):
pass

+ 0
- 4
deemix/app/messageinterface.py View File

@ -1,4 +0,0 @@
class MessageInterface:
def send(self, message, value=None):
"""Implement this class to process updates and messages from the core"""
pass

+ 0
- 115
deemix/app/queueitem.py View File

@ -1,115 +0,0 @@
class QueueItem:
def __init__(self, id=None, bitrate=None, title=None, artist=None, cover=None, explicit=False, size=None, type=None, settings=None, queueItemDict=None):
if queueItemDict:
self.title = queueItemDict['title']
self.artist = queueItemDict['artist']
self.cover = queueItemDict['cover']
self.explicit = queueItemDict.get('explicit', False)
self.size = queueItemDict['size']
self.type = queueItemDict['type']
self.id = queueItemDict['id']
self.bitrate = queueItemDict['bitrate']
self.extrasPath = queueItemDict.get('extrasPath', '')
self.files = queueItemDict['files']
self.downloaded = queueItemDict['downloaded']
self.failed = queueItemDict['failed']
self.errors = queueItemDict['errors']
self.progress = queueItemDict['progress']
self.settings = queueItemDict.get('settings')
else:
self.title = title
self.artist = artist
self.cover = cover
self.explicit = explicit
self.size = size
self.type = type
self.id = id
self.bitrate = bitrate
self.extrasPath = None
self.files = []
self.settings = settings
self.downloaded = 0
self.failed = 0
self.errors = []
self.progress = 0
self.uuid = f"{self.type}_{self.id}_{self.bitrate}"
self.cancel = False
self.ack = None
def toDict(self):
return {
'title': self.title,
'artist': self.artist,
'cover': self.cover,
'explicit': self.explicit,
'size': self.size,
'extrasPath': self.extrasPath,
'files': self.files,
'downloaded': self.downloaded,
'failed': self.failed,
'errors': self.errors,
'progress': self.progress,
'type': self.type,
'id': self.id,
'bitrate': self.bitrate,
'uuid': self.uuid,
'ack': self.ack
}
def getResettedItem(self):
item = self.toDict()
item['downloaded'] = 0
item['failed'] = 0
item['progress'] = 0
item['errors'] = []
return item
def getSlimmedItem(self):
light = self.toDict()
propertiesToDelete = ['single', 'collection', '_EXTRA', 'settings']
for property in propertiesToDelete:
if property in light:
del light[property]
return light
class QISingle(QueueItem):
def __init__(self, id=None, bitrate=None, title=None, artist=None, cover=None, explicit=False, type=None, settings=None, single=None, queueItemDict=None):
if queueItemDict:
super().__init__(queueItemDict=queueItemDict)
self.single = queueItemDict['single']
else:
super().__init__(id, bitrate, title, artist, cover, explicit, 1, type, settings)
self.single = single
def toDict(self):
queueItem = super().toDict()
queueItem['single'] = self.single
return queueItem
class QICollection(QueueItem):
def __init__(self, id=None, bitrate=None, title=None, artist=None, cover=None, explicit=False, size=None, type=None, settings=None, collection=None, queueItemDict=None):
if queueItemDict:
super().__init__(queueItemDict=queueItemDict)
self.collection = queueItemDict['collection']
else:
super().__init__(id, bitrate, title, artist, cover, explicit, size, type, settings)
self.collection = collection
def toDict(self):
queueItem = super().toDict()
queueItem['collection'] = self.collection
return queueItem
class QIConvertable(QICollection):
def __init__(self, id=None, bitrate=None, title=None, artist=None, cover=None, explicit=False, size=None, type=None, settings=None, extra=None, queueItemDict=None):
if queueItemDict:
super().__init__(queueItemDict=queueItemDict)
self.extra = queueItemDict['_EXTRA']
else:
super().__init__(id, bitrate, title, artist, cover, explicit, size, type, settings, [])
self.extra = extra
def toDict(self):
queueItem = super().toDict()
queueItem['_EXTRA'] = self.extra
return queueItem

+ 0
- 569
deemix/app/queuemanager.py View File

@ -1,569 +0,0 @@
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)