Merge pull request 'Refactored code to be Object Oriented where possible' (#22) from dev into main

Reviewed-on: https://codeberg.org/RemixDev/deemix/pulls/22
This commit is contained in:
RemixDev 2020-08-16 12:37:25 +02:00
commit 846a35cd20
19 changed files with 2054 additions and 1975 deletions

View File

@ -1,3 +1,3 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
__version__ = "1.1.31" __version__ = "1.2.0"

View File

@ -1,30 +1,25 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import click import click
import deemix.app.cli as app from deemix.app.cli import cli
from deemix.app.settings import initSettings
from os.path import isfile from os.path import isfile
@click.command() @click.command()
@click.option('-b', '--bitrate', default=None, help='Overwrites the default bitrate selected') @click.option('-b', '--bitrate', default=None, help='Overwrites the default bitrate selected')
@click.option('-l', '--local', is_flag=True, help='Downloads in a local folder insted of using the default') @click.option('-l', '--local', is_flag=True, help='Downloads in a local folder insted of using the default')
@click.argument('url', nargs=-1, required=True) @click.argument('url', nargs=-1, required=True)
def download(bitrate, local, url): def download(bitrate, local, url):
settings = initSettings(local) app = cli(local)
app.login() app.login()
url = list(url) url = list(url)
if isfile(url[0]): if isfile(url[0]):
filename = url[0] filename = url[0]
with open(filename) as f: with open(filename) as f:
url = f.readlines() url = f.readlines()
app.downloadLink(url, settings, bitrate) app.downloadLink(url, bitrate)
click.echo("All done!") click.echo("All done!")
if local: if local:
click.echo(settings['downloadLocation']) #folder name output click.echo(app.set.settings['downloadLocation']) #folder name output
def main():
download()
if __name__ == '__main__': if __name__ == '__main__':
main() download()

View File

@ -257,7 +257,35 @@ 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']
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,2 +1,12 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# Empty File from deemix.api.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):
self.set = Settings(configFolder)
self.dz = Deezer()
self.sp = SpotifyHelper(configFolder)
self.qm = QueueManager()

View File

@ -1,43 +1,47 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os.path as path import os.path as path
import string
import random
from os import mkdir from os import mkdir
from deemix.utils import localpaths from deemix.app import deemix
from deemix.api.deezer import Deezer
from deemix.app.queuemanager import addToQueue
from deemix.app.spotify import SpotifyHelper
dz = Deezer() def randomString(stringLength=8):
sp = SpotifyHelper() letters = string.ascii_lowercase
return ''.join(random.choice(letters) for i in range(stringLength))
class cli(deemix):
def __init__(self, local, configFolder=None):
super().__init__(configFolder)
if local:
self.set.settings['downloadLocation'] = randomString(12)
print("Using a local download folder: "+settings['downloadLocation'])
def requestValidArl(): def downloadLink(self, url, bitrate=None):
for link in url:
if ';' in link:
for l in link.split(";"):
self.qm.addToQueue(self.dz, self.sp, l, self.set.settings, bitrate)
else:
self.qm.addToQueue(self.dz, self.sp, link, self.set.settings, bitrate)
def requestValidArl(self):
while True: while True:
arl = input("Paste here your arl:") arl = input("Paste here your arl:")
if dz.login_via_arl(arl): if self.dz.login_via_arl(arl):
break break
return arl return arl
def login(self):
def login(): configFolder = self.set.configFolder
configFolder = localpaths.getConfigFolder()
if not path.isdir(configFolder): if not path.isdir(configFolder):
mkdir(configFolder) mkdir(configFolder)
if path.isfile(path.join(configFolder, '.arl')): if path.isfile(path.join(configFolder, '.arl')):
with open(path.join(configFolder, '.arl'), 'r') as f: with open(path.join(configFolder, '.arl'), 'r') as f:
arl = f.readline().rstrip("\n") arl = f.readline().rstrip("\n")
if not dz.login_via_arl(arl): if not self.dz.login_via_arl(arl):
arl = requestValidArl() arl = self.requestValidArl()
else: else:
arl = requestValidArl() arl = self.requestValidArl()
with open(path.join(configFolder, '.arl'), 'w') as f: with open(path.join(configFolder, '.arl'), 'w') as f:
f.write(arl) f.write(arl)
def downloadLink(url, settings, bitrate=None):
for link in url:
if ';' in link:
for l in link.split(";"):
addToQueue(dz, sp, l, settings, bitrate)
else:
addToQueue(dz, sp, link, settings, bitrate)

View File

@ -1,75 +0,0 @@
{
"downloadLocation": "",
"tracknameTemplate": "%artist% - %title%",
"albumTracknameTemplate": "%tracknumber% - %title%",
"playlistTracknameTemplate": "%position% - %artist% - %title%",
"createPlaylistFolder": true,
"playlistNameTemplate": "%playlist%",
"createArtistFolder": false,
"artistNameTemplate": "%artist%",
"createAlbumFolder": true,
"albumNameTemplate": "%artist% - %album%",
"createCDFolder": true,
"createStructurePlaylist": false,
"createSingleFolder": false,
"padTracks": true,
"paddingSize": "0",
"illegalCharacterReplacer": "_",
"queueConcurrency": 3,
"maxBitrate": "3",
"fallbackBitrate": true,
"fallbackSearch": false,
"logErrors": true,
"logSearched": false,
"saveDownloadQueue": false,
"overwriteFile": "n",
"createM3U8File": false,
"playlistFilenameTemplate": "playlist",
"syncedLyrics": false,
"embeddedArtworkSize": 800,
"localArtworkSize": 1400,
"localArtworkFormat": "jpg",
"saveArtwork": true,
"coverImageTemplate": "cover",
"saveArtworkArtist": false,
"artistImageTemplate": "folder",
"jpegImageQuality": 80,
"dateFormat": "Y-M-D",
"albumVariousArtists": true,
"removeAlbumVersion": false,
"removeDuplicateArtists": false,
"featuredToTitle": "0",
"titleCasing": "nothing",
"artistCasing": "nothing",
"executeCommand": "",
"tags": {
"title": true,
"artist": true,
"album": true,
"cover": true,
"trackNumber": true,
"trackTotal": false,
"discNumber": true,
"discTotal": false,
"albumArtist": true,
"genre": true,
"year": true,
"date": true,
"explicit": false,
"isrc": true,
"length": true,
"barcode": true,
"bpm": true,
"replayGain": false,
"label": true,
"lyrics": false,
"copyright": false,
"composer": false,
"involvedPeople": false,
"savePlaylistAsCompilation": false,
"useNullSeparator": false,
"saveID3v1": true,
"multiArtistSeparator": "default",
"singleAlbumArtist": false
}
}

File diff suppressed because it is too large Load Diff

637
deemix/app/downloadjob.py Normal file
View File

@ -0,0 +1,637 @@
#!/usr/bin/env python3
import os.path
import re
from requests import get
from requests.exceptions import HTTPError, ConnectionError
from concurrent.futures import ThreadPoolExecutor
from os import makedirs, remove, system as execute
from tempfile import gettempdir
from time import sleep
from deemix.app.queueitem import QIConvertable, QISingle, QICollection
from deemix.app.track import Track
from deemix.utils.misc import changeCase
from deemix.utils.pathtemplates import generateFilename, generateFilepath, settingsRegexAlbum, settingsRegexArtist, settingsRegexPlaylistFile
from deemix.api.deezer import USER_AGENT_HEADER
from deemix.utils.taggers import tagID3, tagFLAC
from Cryptodome.Cipher import Blowfish
from mutagen.flac import FLACNoHeaderError
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger('deemix')
TEMPDIR = os.path.join(gettempdir(), 'deemix-imgs')
if not os.path.isdir(TEMPDIR):
makedirs(TEMPDIR)
extensions = {
9: '.flac',
0: '.mp3',
3: '.mp3',
1: '.mp3',
8: '.mp3',
15: '.mp4',
14: '.mp4',
13: '.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!"
}
def downloadImage(url, path, overwrite="n"):
if not os.path.isfile(path) or overwrite in ['y', 't', 'b']:
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 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")
sleep(1)
return downloadImage(urlBase+pictureUrl.replace(str(pictureSize)+"x"+str(pictureSize), '1200x1200'), path, overwrite)
logger.error("Couldn't download Image: "+url)
except:
sleep(1)
return downloadImage(url, path, overwrite)
remove(path)
return None
else:
return path
def formatDate(date, template):
elements = {
'year': ['YYYY', 'YY', 'Y'],
'month': ['MM', 'M'],
'day': ['DD', 'D']
}
for element, placeholders in elements.items():
for placeholder in placeholders:
if placeholder in template:
template = template.replace(placeholder, str(date[element]))
return template
class DownloadJob:
def __init__(self, dz, sp, queueItem, interface=None):
self.dz = dz
self.sp = sp
self.interface = interface
if isinstance(queueItem, QIConvertable) and queueItem.extra:
self.sp.convert_spotify_playlist(self.dz, queueItem, interface=self.interface)
self.queueItem = queueItem
self.settings = queueItem.settings
self.bitrate = queueItem.bitrate
self.downloadPercentage = 0
self.lastPercentage = 0
self.extrasPath = self.settings['downloadLocation']
self.playlistPath = None
self.playlistURLs = []
def start(self):
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)
with ThreadPoolExecutor(self.settings['queueConcurrency']) as executor:
for pos, track in enumerate(self.queueItem.collection, start=0):
tracks[pos] = executor.submit(self.downloadWrapper, track)
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):
# Save Album Cover
if self.settings['saveArtwork'] and 'albumPath' in result:
for image in result['albumURLs']:
downloadImage(image['url'], f"{result['albumPath']}.{image['ext']}", self.settings['overwriteFile'])
# Save Artist Artwork
if self.settings['saveArtworkArtist'] and 'artistPath' in result:
for image in result['artistURLs']:
downloadImage(image['url'], f"{result['artistPath']}.{image['ext']}", self.settings['overwriteFile'])
# Create searched logfile
if self.settings['logSearched'] and 'searched' in result:
with open(os.path.join(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%", self.extrasPath).replace("%filename%", result['filename']))
def collectionAfterDownload(self, tracks):
playlist = [None] * len(tracks)
errors = ""
searched = ""
for index in range(len(tracks)):
result = tracks[index].result()
# Check if queue is cancelled
if not result:
return None
# Log errors to file
if 'error' in result:
if not 'data' in result['error']:
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'], f"{result['albumPath']}.{image['ext']}", self.settings['overwriteFile'])
# Save Artist Artwork
if self.settings['saveArtworkArtist'] and 'artistPath' in result:
for image in result['artistURLs']:
downloadImage(image['url'], f"{result['artistPath']}.{image['ext']}", self.settings['overwriteFile'])
# Save filename for playlist file
playlist[index] = ""
if 'filename' in result:
playlist[index] = result['filename']
# Create errors logfile
if self.settings['logErrors'] and errors != "":
with open(os.path.join(self.extrasPath, 'errors.txt'), 'wb') as f:
f.write(errors.encode('utf-8'))
# Create searched logfile
if self.settings['logSearched'] and searched != "":
with open(os.path.join(self.extrasPath, 'searched.txt'), 'wb') as f:
f.write(searched.encode('utf-8'))
# Save Playlist Artwork
if self.settings['saveArtwork'] and self.playlistPath and not self.settings['tags']['savePlaylistAsCompilation']:
for image in self.playlistURLs:
downloadImage(image['url'], os.path.join(self.extrasPath, self.playlistPath)+f".{image['ext']}", self.settings['overwriteFile'])
# Create M3U8 File
if self.settings['createM3U8File']:
filename = settingsRegexPlaylistFile(self.settings['playlistFilenameTemplate'], queueItem, self.settings) or "playlist"
with open(os.path.join(self.extrasPath, 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%", self.extrasPath))
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")
track = Track(self.dz,
settings=self.settings,
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
)
if self.queueItem.cancel: raise DownloadCancelled
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.get_track_gw(track.fallbackId)
track.parseEssentialData(self.dz, newTrack)
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.get_track_from_metadata(track.mainArtist['name'], track.title, track.album['title'])
if searchedId != 0:
newTrack = self.dz.get_track_gw(searchedId)
track.parseEssentialData(self.dz, newTrack)
track.searched = True
return self.download(trackAPI_gw, track)
else:
raise DownloadFailed("notEncodedNoAlternative")
else:
raise DownloadFailed("notEncoded")
selectedFormat = self.getPreferredBitrate(track)
if selectedFormat == -100:
if track.fallbackId != "0":
logger.warn(f"[{track.mainArtist['name']} - {track.title}] Track not found at desired bitrate, using fallback id")
newTrack = self.dz.get_track_gw(track.fallbackId)
track.parseEssentialData(self.dz, newTrack)
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.get_track_from_metadata(track.mainArtist['name'], track.title, track.album['title'])
if searchedId != 0:
newTrack = self.dz.get_track_gw(searchedId)
track.parseEssentialData(self.dz, newTrack)
track.searched = True
return self.download(trackAPI_gw, track)
else:
raise DownloadFailed("wrongBitrateNoAlternative")
else:
raise DownloadFailed("wrongBitrate")
elif selectedFormat == -200:
raise DownloadFailed("no360RA")
track.selectedFormat = selectedFormat
if self.settings['tags']['savePlaylistAsCompilation'] and track.playlist:
track.trackNumber = track.position
track.discNumber = "1"
track.album = {**track.album, **track.playlist}
track.album['picPath'] = os.path.join(TEMPDIR, f"pl{trackAPI_gw['_EXTRA_PLAYLIST']['id']}_{self.settings['embeddedArtworkSize']}.jpg")
else:
if track.album['date']:
track.date = track.album['date']
track.album['picUrl'] = "https://e-cdns-images.dzcdn.net/images/cover/{}/{}x{}-{}".format(
track.album['pic'],
self.settings['embeddedArtworkSize'], self.settings['embeddedArtworkSize'],
f'000000-{self.settings["jpegImageQuality"]}-0-0.jpg'
)
track.album['picPath'] = os.path.join(TEMPDIR, f"alb{track.album['id']}_{self.settings['embeddedArtworkSize']}.jpg")
track.album['bitrate'] = selectedFormat
track.dateString = formatDate(track.date, self.settings['dateFormat'])
track.album['dateString'] = formatDate(track.album['date'], self.settings['dateFormat'])
# Check if user wants the feat in the title
# 0 => do not change
# 1 => remove from title
# 2 => add to title
# 3 => remove from title and album title
if self.settings['featuredToTitle'] == "1":
track.title = track.getCleanTitle()
elif self.settings['featuredToTitle'] == "2":
track.title = track.getFeatTitle()
elif self.settings['featuredToTitle'] == "3":
track.title = track.getCleanTitle()
track.album['title'] = track.getCleanAlbumTitle()
# 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()
# Generate artist tag if needed
if self.settings['tags']['multiArtistSeparator'] != "default":
if self.settings['tags']['multiArtistSeparator'] == "andFeat":
track.artistsString = track.mainArtistsString
if track.featArtistsString and self.settings['featuredToTitle'] != "2":
track.artistsString += " " + track.featArtistsString
else:
track.artistsString = self.settings['tags']['multiArtistSeparator'].join(track.artists)
else:
track.artistsString = ", ".join(track.artists)
# 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.artistsString = changeCase(track.artistsString, self.settings['artistCasing'])
for i, artist in enumerate(track.artists):
track.artists[i] = changeCase(artist, self.settings['artistCasing'])
# Generate filename and filepath from metadata
filename = generateFilename(track, trackAPI_gw, self.settings)
(filepath, artistPath, coverPath, extrasPath) = generateFilepath(track, trackAPI_gw, 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['picPath'] = downloadImage(track.album['picUrl'], track.album['picPath'])
# Save local album art
if coverPath:
result['albumURLs'] = []
for format in self.settings['localArtworkFormat'].split(","):
if format in ["png","jpg"]:
url = track.album['picUrl'].replace(
f"{self.settings['embeddedArtworkSize']}x{self.settings['embeddedArtworkSize']}",
f"{self.settings['localArtworkSize']}x{self.settings['localArtworkSize']}")
if format == "png":
url = url[:url.find("000000-")]+"none-100-0-0.png"
result['albumURLs'].append({'url': url, 'ext': format})
result['albumPath'] = os.path.join(coverPath,
f"{settingsRegexAlbum(self.settings['coverImageTemplate'], track.album, self.settings, trackAPI_gw['_EXTRA_PLAYLIST'] if'_EXTRA_PLAYLIST' in trackAPI_gw else None)}")
# Save artist art
if artistPath:
result['artistURLs'] = []
for format in self.settings['localArtworkFormat'].split(","):
if format in ["png","jpg"]:
url = ""
if track.album['mainArtist']['pic'] != "":
url = "https://e-cdns-images.dzcdn.net/images/artist/{}/{}x{}-{}".format(
track.album['mainArtist']['pic'], self.settings['localArtworkSize'], self.settings['localArtworkSize'],
'none-100-0-0.png' if format == "png" else f'000000-{self.settings["jpegImageQuality"]}-0-0.jpg')
elif format == "jpg":
url = "https://e-cdns-images.dzcdn.net/images/artist//{}x{}-{}".format(
self.settings['localArtworkSize'], self.settings['localArtworkSize'], f'000000-{self.settings["jpegImageQuality"]}-0-0.jpg')
if url:
result['artistURLs'].append({'url': url, 'ext': format})
result['artistPath'] = os.path.join(artistPath,
f"{settingsRegexArtist(self.settings['artistImageTemplate'], track.album['mainArtist'], self.settings)}")
# Remove subfolders from filename and add it to filepath
if os.path.sep in filename:
tempPath = filename[:filename.rfind(os.path.sep)]
filepath = os.path.join(filepath, tempPath)
filename = filename[filename.rfind(os.path.sep) + len(os.path.sep):]
# Make sure the filepath exsists
makedirs(filepath, exist_ok=True)
writepath = os.path.join(filepath, filename + extensions[track.selectedFormat])
# Save lyrics in lrc file
if self.settings['syncedLyrics'] and 'sync' in track.lyrics:
if not os.path.isfile(os.path.join(filepath, filename + '.lrc')) or settings['overwriteFile'] in ['y', 't']:
with open(os.path.join(filepath, filename + '.lrc'), 'wb') as f:
f.write(track.lyrics['sync'].encode('utf-8'))
trackAlreadyDownloaded = os.path.isfile(writepath)
if trackAlreadyDownloaded and self.settings['overwriteFile'] == 'b':
baseFilename = os.path.join(filepath, filename)
i = 1
currentFilename = baseFilename+' ('+str(i)+')'+ extensions[track.selectedFormat]
while os.path.isfile(currentFilename):
i += 1
currentFilename = baseFilename+' ('+str(i)+')'+ extensions[track.selectedFormat]
trackAlreadyDownloaded = False
writepath = currentFilename
if extrasPath:
if not self.extrasPath:
self.extrasPath = extrasPath
result['extrasPath'] = extrasPath
# Data for m3u file
result['filename'] = writepath[len(extrasPath):]
# Save playlist cover
if track.playlist:
if not len(self.playlistURLs):
if 'dzcdn.net' in track.playlist['picUrl']:
for format in self.settings['localArtworkFormat'].split(","):
if format in ["png","jpg"]:
url = track.playlist['picUrl'].replace(
f"{self.settings['embeddedArtworkSize']}x{self.settings['embeddedArtworkSize']}",
f"{self.settings['localArtworkSize']}x{self.settings['localArtworkSize']}")
if format == "png":
url = url[:url.find("000000-")]+"none-100-0-0.png"
self.playlistURLs.append({'url': url, 'ext': format})
else:
self.playlistURLs.append({'url': track.playlist['picUrl'], 'ext': 'jpg'})
if not self.playlistPath:
track.playlist['id'] = "pl_" + str(trackAPI_gw['_EXTRA_PLAYLIST']['id'])
track.playlist['genre'] = ["Compilation", ]
track.playlist['bitrate'] = selectedFormat
track.playlist['dateString'] = formatDate(track.playlist['date'], self.settings['dateFormat'])
self.playlistPath = f"{settingsRegexAlbum(self.settings['coverImageTemplate'], track.playlist, self.settings, trackAPI_gw['_EXTRA_PLAYLIST'])}"
if not trackAlreadyDownloaded or self.settings['overwriteFile'] == 'y':
logger.info(f"[{track.mainArtist['name']} - {track.title}] Downloading the track")
track.downloadUrl = self.dz.get_track_stream_url(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:
remove(writepath)
raise DownloadCancelled
except (HTTPError, DownloadEmpty):
remove(writepath)
if track.fallbackId != "0":
logger.warn(f"[{track.mainArtist['name']} - {track.title}] Track not available, using fallback id")
newTrack = self.dz.get_track_gw(track.fallbackId)
track.parseEssentialData(self.dz, newTrack)
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.get_track_from_metadata(track.mainArtist['name'], track.title, track.album['title'])
if searchedId != 0:
newTrack = self.dz.get_track_gw(searchedId)
track.parseEssentialData(self.dz, newTrack)
track.searched = True
return False
else:
raise DownloadFailed("notAvailableNoAlternative")
else:
raise DownloadFailed("notAvailable")
except ConnectionError as e:
logger.exception(str(e))
logger.warn(f"[{track.mainArtist['name']} - {track.title}] Error while downloading the track, trying again in 5s...")
sleep(5)
return downloadMusic(track, trackAPI_gw)
except Exception as e:
logger.exception(str(e))
logger.warn(f"[{track.mainArtist['name']} - {track.title}] Error while downloading the track, you should report this to the developers")
raise e
return True
try:
trackDownloaded = downloadMusic(track, trackAPI_gw)
except DownloadFailed as e:
raise e
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 ['t', 'y']) and not track.localTrack:
logger.info(f"[{track.mainArtist['name']} - {track.title}] Applying tags to the track")
if track.selectedFormat in [3, 1, 8]:
tagID3(writepath, track, self.settings['tags'])
elif track.selectedFormat == 9:
try:
tagFLAC(writepath, track, self.settings['tags'])
except FLACNoHeaderError:
remove(writepath)
logger.warn(f"[{track.mainArtist['name']} - {track.title}] Track not available in FLAC, falling back if necessary")
self.removeTrackPercentage()
track.filesizes['FILESIZE_FLAC'] = "0"
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")
self.queueItem.downloaded += 1
if self.interface:
self.interface.send("updateQueue", {'uuid': self.queueItem.uuid, 'downloaded': True, 'downloadPath': writepath})
return result
def getPreferredBitrate(self, track):
if track.localTrack:
return 0
fallback = self.settings['fallbackBitrate']
formats_non_360 = {
9: "FLAC",
3: "MP3_320",
1: "MP3_128",
}
formats_360 = {
15: "MP4_RA3",
14: "MP4_RA2",
13: "MP4_RA1",
}
if not fallback:
error_num = -100
formats = formats_360
formats.update(formats_non_360)
elif int(self.bitrate) in formats_360:
error_num = -200
formats = formats_360
else:
error_num = 8
formats = formats_non_360
for format_num, format in formats.items():
if format_num <= int(self.bitrate):
if f"FILESIZE_{format}" in track.filesizes and int(track.filesizes[f"FILESIZE_{format}"]) != 0:
return format_num
else:
if fallback:
continue
else:
return error_num
return error_num # fallback is enabled and loop went through all formats
def streamTrack(self, stream, track):
if self.queueItem.cancel: raise DownloadCancelled
try:
request = get(track.downloadUrl, headers=self.dz.http_headers, stream=True, timeout=30)
except ConnectionError:
sleep(2)
return self.streamTrack(stream, track)
request.raise_for_status()
blowfish_key = str.encode(self.dz._get_blowfish_key(str(track.id)))
complete = int(request.headers["Content-Length"])
if complete == 0:
raise DownloadEmpty
chunkLength = 0
percentage = 0
i = 0
for chunk in request.iter_content(2048):
if self.queueItem.cancel: raise DownloadCancelled
if i % 3 == 0 and len(chunk) == 2048:
chunk = Blowfish.new(blowfish_key, Blowfish.MODE_CBC, b"\x00\x01\x02\x03\x04\x05\x06\x07").decrypt(chunk)
stream.write(chunk)
chunkLength += len(chunk)
if isinstance(self.queueItem, QISingle):
percentage = (chunkLength / complete) * 100
self.downloadPercentage = percentage
else:
chunkProgres = (len(chunk) / complete) / self.queueItem.size * 100
self.downloadPercentage += chunkProgres
self.updatePercentage()
i += 1
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'] + (trackAPI_gw['VERSION'] if 'VERSION' in trackAPI_gw and trackAPI_gw['VERSION'] and not trackAPI_gw['VERSION'] in trackAPI_gw['SNG_TITLE'] else ""),
'artist': trackAPI_gw['ART_NAME']
}
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

108
deemix/app/queueitem.py Normal file
View File

@ -0,0 +1,108 @@
#!/usr/bin/env python3
class QueueItem:
def __init__(self, id=None, bitrate=None, title=None, artist=None, cover=None, size=None, type=None, settings=None, queueItemDict=None):
if queueItemDict:
self.title = queueItemDict['title']
self.artist = queueItemDict['artist']
self.cover = queueItemDict['cover']
self.size = queueItemDict['size']
self.type = queueItemDict['type']
self.id = queueItemDict['id']
self.bitrate = queueItemDict['bitrate']
self.downloaded = queueItemDict['downloaded']
self.failed = queueItemDict['failed']
self.errors = queueItemDict['errors']
self.progress = queueItemDict['progress']
self.settings = None
if 'settings' in queueItemDict:
self.settings = queueItemDict['settings']
else:
self.title = title
self.artist = artist
self.cover = cover
self.size = size
self.type = type
self.id = id
self.bitrate = bitrate
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
def toDict(self):
return {
'title': self.title,
'artist': self.artist,
'cover': self.cover,
'size': self.size,
'downloaded': self.downloaded,
'failed': self.failed,
'errors': self.errors,
'progress': self.progress,
'type': self.type,
'id': self.id,
'bitrate': self.bitrate,
'uuid': self.uuid
}
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, 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, 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, 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, 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, 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, size, type, settings, [])
self.extra = extra
def toDict(self):
queueItem = super().toDict()
queueItem['_EXTRA'] = self.extra
return queueItem

View File

@ -1,75 +1,36 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from deemix.app.downloader import download from deemix.app.downloadjob import DownloadJob
from deemix.utils.misc import getIDFromLink, getTypeFromLink, getBitrateInt from deemix.utils.misc import getIDFromLink, getTypeFromLink, getBitrateInt
from deemix.api.deezer import APIError from deemix.api.deezer import APIError
from spotipy.exceptions import SpotifyException from spotipy.exceptions import SpotifyException
from deemix.app.queueitem import QISingle, QICollection, QIConvertable
import logging import logging
import os.path as path
import json import json
from os import remove
from time import sleep
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
logger = logging.getLogger('deemix') logger = logging.getLogger('deemix')
queue = [] class QueueManager:
queueList = {} def __init__(self):
queueComplete = [] self.queue = []
currentItem = "" self.queueList = {}
self.queueComplete = []
self.currentItem = ""
""" def generateQueueItem(self, dz, sp, url, settings, bitrate=None, albumAPI=None, interface=None):
queueItem base structure
title
artist
cover
size
downloaded
failed
errors
progress
type
id
bitrate
uuid: type+id+bitrate
if its a single track
single
if its an album/playlist
collection
"""
def resetQueueItems(items, q):
result = {}
for item in items.keys():
result[item] = items[item].copy()
if item in q:
result[item]['downloaded'] = 0
result[item]['failed'] = 0
result[item]['progress'] = 0
result[item]['errors'] = []
return result
def slimQueueItems(items):
result = {}
for item in items.keys():
result[item] = slimQueueItem(items[item])
return result
def slimQueueItem(item):
light = item.copy()
if 'single' in light:
del light['single']
if 'collection' in light:
del light['collection']
return light
def generateQueueItem(dz, sp, url, settings, bitrate=None, albumAPI=None, interface=None):
forcedBitrate = getBitrateInt(bitrate) forcedBitrate = getBitrateInt(bitrate)
bitrate = forcedBitrate if forcedBitrate else settings['maxBitrate'] bitrate = forcedBitrate if forcedBitrate else settings['maxBitrate']
type = getTypeFromLink(url) type = getTypeFromLink(url)
id = getIDFromLink(url, type) id = getIDFromLink(url, type)
result = {}
result['link'] = url
if type == None or id == None: if type == None or id == None:
logger.warn("URL not recognized") logger.warn("URL not recognized")
result['error'] = "URL not recognized" return QueueError(url, "URL not recognized", "invalidURL")
result['errid'] = "invalidURL"
elif type == "track": elif type == "track":
if id.startswith("isrc"): if id.startswith("isrc"):
try: try:
@ -77,21 +38,18 @@ def generateQueueItem(dz, sp, url, settings, bitrate=None, albumAPI=None, interf
if 'id' in trackAPI and 'title' in trackAPI: if 'id' in trackAPI and 'title' in trackAPI:
id = trackAPI['id'] id = trackAPI['id']
else: else:
result['error'] = "Track ISRC is not available on deezer" return QueueError(url, "Track ISRC is not available on deezer", "ISRCnotOnDeezer")
result['errid'] = "ISRCnotOnDeezer"
return result
except APIError as e: except APIError as e:
e = json.loads(str(e)) e = json.loads(str(e))
result['error'] = 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 ''}")
return result
try: try:
trackAPI = dz.get_track_gw(id) trackAPI = dz.get_track_gw(id)
except APIError as e: except APIError as e:
e = json.loads(str(e)) e = json.loads(str(e))
result['error'] = "Wrong URL" message = "Wrong URL"
if "DATA_ERROR" in e: if "DATA_ERROR" in e:
result['error'] += f": {e['DATA_ERROR']}" message += f": {e['DATA_ERROR']}"
return result return QueueError(url, message)
if albumAPI: if albumAPI:
trackAPI['_EXTRA_ALBUM'] = albumAPI trackAPI['_EXTRA_ALBUM'] = albumAPI
if settings['createSingleFolder']: if settings['createSingleFolder']:
@ -100,31 +58,26 @@ def generateQueueItem(dz, sp, url, settings, bitrate=None, albumAPI=None, interf
trackAPI['FILENAME_TEMPLATE'] = settings['tracknameTemplate'] trackAPI['FILENAME_TEMPLATE'] = settings['tracknameTemplate']
trackAPI['SINGLE_TRACK'] = True trackAPI['SINGLE_TRACK'] = True
result['title'] = trackAPI['SNG_TITLE'] title = trackAPI['SNG_TITLE']
if 'VERSION' in trackAPI and trackAPI['VERSION']: if 'VERSION' in trackAPI and trackAPI['VERSION']:
result['title'] += " " + trackAPI['VERSION'] title += " " + trackAPI['VERSION']
result['artist'] = trackAPI['ART_NAME'] return QISingle(
result[ id,
'cover'] = f"https://e-cdns-images.dzcdn.net/images/cover/{trackAPI['ALB_PICTURE']}/75x75-000000-80-0-0.jpg" bitrate,
result['size'] = 1 title,
result['downloaded'] = 0 trackAPI['ART_NAME'],
result['failed'] = 0 f"https://e-cdns-images.dzcdn.net/images/cover/{trackAPI['ALB_PICTURE']}/75x75-000000-80-0-0.jpg",
result['errors'] = [] 'track',
result['progress'] = 0 settings,
result['type'] = 'track' trackAPI,
result['id'] = id )
result['bitrate'] = bitrate
result['uuid'] = f"{result['type']}_{id}_{bitrate}"
result['settings'] = settings or {}
result['single'] = trackAPI
elif type == "album": elif type == "album":
try: try:
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))
result['error'] = 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 ''}")
return result
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)
@ -137,96 +90,54 @@ def generateQueueItem(dz, sp, url, settings, bitrate=None, albumAPI=None, interf
if albumAPI['nb_tracks'] == 255: if albumAPI['nb_tracks'] == 255:
albumAPI['nb_tracks'] = len(tracksArray) albumAPI['nb_tracks'] = len(tracksArray)
result['title'] = albumAPI['title']
result['artist'] = albumAPI['artist']['name']
if albumAPI['cover_small'] != None: if albumAPI['cover_small'] != None:
result['cover'] = albumAPI['cover_small'][:-24] + '/75x75-000000-80-0-0.jpg' cover = albumAPI['cover_small'][:-24] + '/75x75-000000-80-0-0.jpg'
else: else:
result['cover'] = f"https://e-cdns-images.dzcdn.net/images/cover/{albumAPI_gw['ALB_PICTURE']}/75x75-000000-80-0-0.jpg" cover = f"https://e-cdns-images.dzcdn.net/images/cover/{albumAPI_gw['ALB_PICTURE']}/75x75-000000-80-0-0.jpg"
result['size'] = albumAPI['nb_tracks']
result['downloaded'] = 0
result['failed'] = 0
result['errors'] = []
result['progress'] = 0
result['type'] = 'album'
result['id'] = id
result['bitrate'] = bitrate
result['uuid'] = f"{result['type']}_{id}_{bitrate}"
result['settings'] = settings or {}
totalSize = len(tracksArray) totalSize = len(tracksArray)
result['collection'] = [] collection = []
for pos, trackAPI in enumerate(tracksArray, start=1): for pos, trackAPI in enumerate(tracksArray, start=1):
trackAPI['_EXTRA_ALBUM'] = albumAPI trackAPI['_EXTRA_ALBUM'] = albumAPI
trackAPI['POSITION'] = pos trackAPI['POSITION'] = pos
trackAPI['SIZE'] = totalSize trackAPI['SIZE'] = totalSize
trackAPI['FILENAME_TEMPLATE'] = settings['albumTracknameTemplate'] trackAPI['FILENAME_TEMPLATE'] = settings['albumTracknameTemplate']
result['collection'].append(trackAPI) collection.append(trackAPI)
return QICollection(
id,
bitrate,
albumAPI['title'],
albumAPI['artist']['name'],
cover,
totalSize,
'album',
settings,
collection,
)
elif type == "playlist": elif type == "playlist":
try: try:
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))
result['error'] = "Wrong URL" message = "Wrong URL"
if "DATA_ERROR" in e: if "DATA_ERROR" in e:
result['error'] += f": {e['DATA_ERROR']}" message += f": {e['DATA_ERROR']}"
return result 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.")
result['error'] = "You can't download others private playlists." return QueueError(url, "You can't download others private playlists.", "notYourPrivatePlaylist")
result['errid'] = "notYourPrivatePlaylist"
return result
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)
result['title'] = playlistAPI['title']
result['artist'] = playlistAPI['creator']['name']
result['cover'] = playlistAPI['picture_small'][:-24] + '/75x75-000000-80-0-0.jpg'
result['size'] = playlistAPI['nb_tracks']
result['downloaded'] = 0
result['failed'] = 0
result['errors'] = []
result['progress'] = 0
result['type'] = 'playlist'
result['id'] = id
result['bitrate'] = bitrate
result['uuid'] = f"{result['type']}_{id}_{bitrate}"
result['settings'] = settings or {}
totalSize = len(playlistTracksAPI) totalSize = len(playlistTracksAPI)
result['collection'] = [] collection = []
for pos, trackAPI in enumerate(playlistTracksAPI, start=1): for pos, trackAPI in enumerate(playlistTracksAPI, start=1):
if 'EXPLICIT_TRACK_CONTENT' in trackAPI and 'EXPLICIT_LYRICS_STATUS' in trackAPI['EXPLICIT_TRACK_CONTENT'] and trackAPI['EXPLICIT_TRACK_CONTENT']['EXPLICIT_LYRICS_STATUS'] in [1,4]: if 'EXPLICIT_TRACK_CONTENT' in trackAPI and 'EXPLICIT_LYRICS_STATUS' in trackAPI['EXPLICIT_TRACK_CONTENT'] and trackAPI['EXPLICIT_TRACK_CONTENT']['EXPLICIT_LYRICS_STATUS'] in [1,4]:
playlistAPI['explicit'] = True playlistAPI['explicit'] = True
@ -234,51 +145,70 @@ def generateQueueItem(dz, sp, url, settings, bitrate=None, albumAPI=None, interf
trackAPI['POSITION'] = pos trackAPI['POSITION'] = pos
trackAPI['SIZE'] = totalSize trackAPI['SIZE'] = totalSize
trackAPI['FILENAME_TEMPLATE'] = settings['playlistTracknameTemplate'] trackAPI['FILENAME_TEMPLATE'] = settings['playlistTracknameTemplate']
result['collection'].append(trackAPI) collection.append(trackAPI)
if not 'explicit' in playlistAPI: if not 'explicit' in playlistAPI:
playlistAPI['explicit'] = False playlistAPI['explicit'] = False
return QICollection(
id,
bitrate,
playlistAPI['title'],
playlistAPI['creator']['name'],
playlistAPI['picture_small'][:-24] + '/75x75-000000-80-0-0.jpg',
totalSize,
'playlist',
settings,
collection,
)
elif type == "artist": elif type == "artist":
try: try:
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))
result['error'] = 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 ''}")
return result
if interface: if interface:
interface.send("startAddingArtist", {'name': artistAPI['name'], 'id': artistAPI['id']}) interface.send("startAddingArtist", {'name': artistAPI['name'], 'id': artistAPI['id']})
artistAPITracks = dz.get_artist_albums(id) artistAPITracks = dz.get_artist_albums(id)
albumList = [] albumList = []
for album in artistAPITracks['data']: for album in artistAPITracks['data']:
albumList.append(generateQueueItem(dz, sp, album['link'], settings, bitrate)) albumList.append(generateQueueItem(dz, sp, album['link'], settings, bitrate))
if interface: if interface:
interface.send("finishAddingArtist", {'name': artistAPI['name'], 'id': artistAPI['id']}) interface.send("finishAddingArtist", {'name': artistAPI['name'], 'id': artistAPI['id']})
return albumList return albumList
elif type == "artistdiscography": elif type == "artistdiscography":
try: try:
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))
result['error'] = 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 ''}")
return result
if interface: if interface:
interface.send("startAddingArtist", {'name': artistAPI['name'], 'id': artistAPI['id']}) interface.send("startAddingArtist", {'name': artistAPI['name'], 'id': artistAPI['id']})
artistDiscographyAPI = dz.get_artist_discography_gw(id, 100) artistDiscographyAPI = dz.get_artist_discography_gw(id, 100)
albumList = [] albumList = []
for type in artistDiscographyAPI: for type in artistDiscographyAPI:
if type != 'all': if type != 'all':
for album in artistDiscographyAPI[type]: for album in artistDiscographyAPI[type]:
albumList.append(generateQueueItem(dz, sp, album['link'], settings, bitrate)) albumList.append(generateQueueItem(dz, sp, album['link'], settings, bitrate))
if interface: if interface:
interface.send("finishAddingArtist", {'name': artistAPI['name'], 'id': artistAPI['id']}) interface.send("finishAddingArtist", {'name': artistAPI['name'], 'id': artistAPI['id']})
return albumList return albumList
elif type == "artisttop": elif type == "artisttop":
try: try:
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))
result['error'] = 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 ''}")
return result
playlistAPI = { playlistAPI = {
'id': str(artistAPI['id'])+"_top_track", 'id': str(artistAPI['id'])+"_top_track",
@ -312,21 +242,8 @@ def generateQueueItem(dz, sp, url, settings, bitrate=None, albumAPI=None, interf
playlistAPI['various_artist'] = dz.get_artist(5080) playlistAPI['various_artist'] = dz.get_artist(5080)
playlistAPI['nb_tracks'] = len(artistTopTracksAPI_gw) playlistAPI['nb_tracks'] = len(artistTopTracksAPI_gw)
result['title'] = playlistAPI['title']
result['artist'] = playlistAPI['creator']['name']
result['cover'] = playlistAPI['picture_small'][:-24] + '/75x75-000000-80-0-0.jpg'
result['size'] = playlistAPI['nb_tracks']
result['downloaded'] = 0
result['failed'] = 0
result['errors'] = []
result['progress'] = 0
result['type'] = 'playlist'
result['id'] = id
result['bitrate'] = bitrate
result['uuid'] = f"{result['type']}_{id}_{bitrate}"
result['settings'] = settings or {}
totalSize = len(artistTopTracksAPI_gw) totalSize = len(artistTopTracksAPI_gw)
result['collection'] = [] collection = []
for pos, trackAPI in enumerate(artistTopTracksAPI_gw, start=1): for pos, trackAPI in enumerate(artistTopTracksAPI_gw, start=1):
if 'EXPLICIT_TRACK_CONTENT' in trackAPI and 'EXPLICIT_LYRICS_STATUS' in trackAPI['EXPLICIT_TRACK_CONTENT'] and trackAPI['EXPLICIT_TRACK_CONTENT']['EXPLICIT_LYRICS_STATUS'] in [1,4]: if 'EXPLICIT_TRACK_CONTENT' in trackAPI and 'EXPLICIT_LYRICS_STATUS' in trackAPI['EXPLICIT_TRACK_CONTENT'] and trackAPI['EXPLICIT_TRACK_CONTENT']['EXPLICIT_LYRICS_STATUS'] in [1,4]:
playlistAPI['explicit'] = True playlistAPI['explicit'] = True
@ -334,200 +251,261 @@ def generateQueueItem(dz, sp, url, settings, bitrate=None, albumAPI=None, interf
trackAPI['POSITION'] = pos trackAPI['POSITION'] = pos
trackAPI['SIZE'] = totalSize trackAPI['SIZE'] = totalSize
trackAPI['FILENAME_TEMPLATE'] = settings['playlistTracknameTemplate'] trackAPI['FILENAME_TEMPLATE'] = settings['playlistTracknameTemplate']
result['collection'].append(trackAPI) collection.append(trackAPI)
if not 'explicit' in playlistAPI: if not 'explicit' in playlistAPI:
playlistAPI['explicit'] = False playlistAPI['explicit'] = False
return QICollection(
id,
bitrate,
playlistAPI['title'],
playlistAPI['creator']['name'],
playlistAPI['picture_small'][:-24] + '/75x75-000000-80-0-0.jpg',
totalSize,
'playlist',
settings,
collection,
)
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.")
result['error'] = "Spotify Features is not setted up correctly." return QueueError(url, "Spotify Features is not setted up correctly.", "spotifyDisabled")
result['errid'] = "spotifyDisabled"
return result
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:
result['error'] = "Wrong URL: "+e.msg[e.msg.find('\n')+2:] return QueueError(url, "Wrong URL: "+e.msg[e.msg.find('\n')+2:])
return result
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!")
result['error'] = "Track not found on deezer!" return QueueError(url, "Track not found on deezer!", "trackNotOnDeezer")
result['errid'] = "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.")
result['error'] = "Spotify Features is not setted up correctly." return QueueError(url, "Spotify Features is not setted up correctly.", "spotifyDisabled")
result['errid'] = "spotifyDisabled"
return result
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:
result['error'] = "Wrong URL: "+e.msg[e.msg.find('\n')+2:] return QueueError(url, "Wrong URL: "+e.msg[e.msg.find('\n')+2:])
return result
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!")
result['error'] = "Album not found on deezer!" return QueueError(url, "Album not found on deezer!", "albumNotOnDeezer")
result['errid'] = "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.")
result['error'] = "Spotify Features is not setted up correctly." return QueueError(url, "Spotify Features is not setted up correctly.", "spotifyDisabled")
result['errid'] = "spotifyDisabled"
return result
if interface:
interface.send("startConvertingSpotifyPlaylist", str(id))
try: try:
playlist = sp.convert_spotify_playlist(dz, id, settings) return sp.generate_playlist_queueitem(dz, id, bitrate, settings)
except SpotifyException as e: except SpotifyException as e:
result['error'] = "Wrong URL: "+e.msg[e.msg.find('\n')+2:] return QueueError(url, "Wrong URL: "+e.msg[e.msg.find('\n')+2:])
return result
playlist['bitrate'] = bitrate
playlist['uuid'] = f"{playlist['type']}_{id}_{bitrate}"
result = playlist
if interface:
interface.send("finishConvertingSpotifyPlaylist", str(id))
else: else:
logger.warn("URL not supported yet") logger.warn("URL not supported yet")
result['error'] = "URL not supported yet" return QueueError(url, "URL not supported yet", "unsupportedURL")
result['errid'] = "unsupportedURL"
return result
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()
if url == "":
return False return False
logger.info("Generating queue item for: "+url) else:
queueItem = generateQueueItem(dz, sp, url, settings, bitrate, interface=interface) queueItem = parseLink(url)
if not queueItem:
return False
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, interface)
self.nextItem(dz, sp, interface)
return True return True
def nextItem(self, dz, sp, interface=None):
def nextItem(dz, 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, queueList[currentItem], interface) DownloadJob(dz, sp, self.queueList[self.currentItem], interface).start()
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['interface']) self.nextItem(dz, sp, interface)
def getQueue(): def getQueue(self):
global currentItem, queueList, queue, queueComplete return (self.queue, self.queueComplete, self.slimQueueList(), self.currentItem)
return (queue, queueComplete, queueList, currentItem)
def saveQueue(self, configFolder):
if len(self.queueList) > 0:
if self.currentItem != "":
self.queue.insert(0, self.currentItem)
with open(path.join(configFolder, 'queue.json'), 'w') as f:
json.dump({
'queue': self.queue,
'queueComplete': self.queueComplete,
'queueList': self.exportQueueList()
}, f)
def restoreQueue(pqueue, pqueueComplete, pqueueList, dz, interface): def exportQueueList(self):
global currentItem, queueList, queue, queueComplete queueList = {}
queueComplete = pqueueComplete for uuid in self.queueList:
queueList = pqueueList if uuid in self.queue:
queue = pqueue queueList[uuid] = self.queueList[uuid].getResettedItem()
nextItem(dz, interface) 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 removeFromQueue(uuid, interface=None): def loadQueue(self, configFolder, settings, interface=None):
global currentItem, queueList, queue, queueComplete if path.isfile(path.join(configFolder, 'queue.json')) and not len(self.queue):
if uuid == currentItem:
if interface: if interface:
interface.send("cancellingCurrentItem", currentItem) interface.send('restoringQueue')
queueList[uuid]['cancel'] = True with open(path.join(configFolder, 'queue.json'), 'r') as f:
elif uuid in queue: qd = json.load(f)
queue.remove(uuid) remove(path.join(configFolder, 'queue.json'))
del queueList[uuid] 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):
self.queue = queue
self.queueComplete = queueComplete
self.queueList = {}
for uuid in queueList:
if 'single' in queueList[uuid]:
self.queueList[uuid] = QISingle(queueItemDict = queueList[uuid])
if 'collection' in queueList[uuid]:
self.queueList[uuid] = QICollection(queueItemDict = queueList[uuid])
if '_EXTRA' in queueList[uuid]:
self.queueList[uuid] = QIConvertable(queueItemDict = queueList[uuid])
self.queueList[uuid].settings = settings
def removeFromQueue(self, uuid, interface=None):
if uuid == self.currentItem:
if interface:
interface.send("cancellingCurrentItem", uuid)
self.queueList[uuid].cancel = True
elif uuid in self.queue:
self.queue.remove(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:
def __init__(self, link, message, errid=None):
self.link = link
self.message = message
self.errid = errid
def toDict(self):
return {
'link': self.link,
'error': self.message,
'errid': self.errid
}

View File

@ -3,8 +3,6 @@ import json
import os.path as path import os.path as path
from os import makedirs, listdir, remove from os import makedirs, listdir, remove
from deemix import __version__ as deemixVersion from deemix import __version__ as deemixVersion
import random
import string
import logging import logging
import datetime import datetime
import platform import platform
@ -14,93 +12,151 @@ logger = logging.getLogger('deemix')
import deemix.utils.localpaths as localpaths import deemix.utils.localpaths as localpaths
settings = {} class Settings:
defaultSettings = {} def __init__(self, configFolder=None):
configDir = "" self.settings = {}
self.configFolder = configFolder
if not self.configFolder:
self.configFolder = localpaths.getConfigFolder()
self.defaultSettings = {
"downloadLocation": path.join(localpaths.getHomeFolder(), 'deemix Music'),
"tracknameTemplate": "%artist% - %title%",
"albumTracknameTemplate": "%tracknumber% - %title%",
"playlistTracknameTemplate": "%position% - %artist% - %title%",
"createPlaylistFolder": True,
"playlistNameTemplate": "%playlist%",
"createArtistFolder": False,
"artistNameTemplate": "%artist%",
"createAlbumFolder": True,
"albumNameTemplate": "%artist% - %album%",
"createCDFolder": True,
"createStructurePlaylist": False,
"createSingleFolder": False,
"padTracks": True,
"paddingSize": "0",
"illegalCharacterReplacer": "_",
"queueConcurrency": 3,
"maxBitrate": "3",
"fallbackBitrate": True,
"fallbackSearch": False,
"logErrors": True,
"logSearched": False,
"saveDownloadQueue": False,
"overwriteFile": "n",
"createM3U8File": False,
"playlistFilenameTemplate": "playlist",
"syncedLyrics": False,
"embeddedArtworkSize": 800,
"localArtworkSize": 1400,
"localArtworkFormat": "jpg",
"saveArtwork": True,
"coverImageTemplate": "cover",
"saveArtworkArtist": False,
"artistImageTemplate": "folder",
"jpegImageQuality": 80,
"dateFormat": "Y-M-D",
"albumVariousArtists": True,
"removeAlbumVersion": False,
"removeDuplicateArtists": False,
"featuredToTitle": "0",
"titleCasing": "nothing",
"artistCasing": "nothing",
"executeCommand": "",
"tags": {
"title": True,
"artist": True,
"album": True,
"cover": True,
"trackNumber": True,
"trackTotal": False,
"discNumber": True,
"discTotal": False,
"albumArtist": True,
"genre": True,
"year": True,
"date": True,
"explicit": False,
"isrc": True,
"length": True,
"barcode": True,
"bpm": True,
"replayGain": False,
"label": True,
"lyrics": False,
"copyright": False,
"composer": False,
"involvedPeople": False,
"savePlaylistAsCompilation": False,
"useNullSeparator": False,
"saveID3v1": True,
"multiArtistSeparator": "default",
"singleAlbumArtist": False
}
}
def initSettings(localFolder = False, configFolder = None): # Create config folder if it doesn't exsist
global settings makedirs(self.configFolder, exist_ok=True)
global defaultSettings
global configDir
currentFolder = path.abspath(path.dirname(__file__))
if not configFolder:
configFolder = localpaths.getConfigFolder()
configDir = configFolder
makedirs(configFolder, exist_ok=True)
with open(path.join(currentFolder, 'default.json'), 'r') as d:
defaultSettings = json.load(d)
defaultSettings['downloadLocation'] = path.join(localpaths.getHomeFolder(), 'deemix Music')
if not path.isfile(path.join(configFolder, 'config.json')):
with open(path.join(configFolder, 'config.json'), 'w') as f:
json.dump(defaultSettings, f, indent=2)
with open(path.join(configFolder, 'config.json'), 'r') as configFile:
settings = json.load(configFile)
settingsCheck()
if localFolder: # Create config file if it doesn't exsist
settings['downloadLocation'] = randomString(12) if not path.isfile(path.join(self.configFolder, 'config.json')):
logger.info("Using a local download folder: "+settings['downloadLocation']) with open(path.join(self.configFolder, 'config.json'), 'w') as f:
elif settings['downloadLocation'] == "": json.dump(self.defaultSettings, f, indent=2)
settings['downloadLocation'] = path.join(localpaths.getHomeFolder(), 'deemix Music')
saveSettings(settings)
makedirs(settings['downloadLocation'], exist_ok=True)
# logfiles # Read config file
# logfile name with open(path.join(self.configFolder, 'config.json'), 'r') as configFile:
logspath = path.join(configFolder, 'logs') self.settings = json.load(configFile)
self.settingsCheck()
# Make sure the download path exsits
makedirs(self.settings['downloadLocation'], exist_ok=True)
# LOGFILES
# Create logfile name and path
logspath = path.join(self.configFolder, 'logs')
now = datetime.datetime.now() now = datetime.datetime.now()
logfile = now.strftime("%Y-%m-%d_%H%M%S")+".log" logfile = now.strftime("%Y-%m-%d_%H%M%S")+".log"
makedirs(logspath, exist_ok=True) makedirs(logspath, exist_ok=True)
# add handler for logfile
# Add handler for logging
fh = logging.FileHandler(path.join(logspath, logfile)) fh = logging.FileHandler(path.join(logspath, logfile))
fh.setLevel(logging.DEBUG) fh.setLevel(logging.DEBUG)
fh.setFormatter(logging.Formatter('%(asctime)s - [%(levelname)s] %(message)s')) fh.setFormatter(logging.Formatter('%(asctime)s - [%(levelname)s] %(message)s'))
logger.addHandler(fh) logger.addHandler(fh)
logger.info(f"{platform.platform(True, True)} - Python {platform.python_version()}, deemix {deemixVersion}") logger.info(f"{platform.platform(True, True)} - Python {platform.python_version()}, deemix {deemixVersion}")
#delete old logfiles
# Only keep last 5 logfiles (to preserve disk space)
logslist = listdir(logspath) logslist = listdir(logspath)
logslist.sort() logslist.sort()
if len(logslist)>5: if len(logslist)>5:
for i in range(len(logslist)-5): for i in range(len(logslist)-5):
remove(path.join(logspath, logslist[i])) remove(path.join(logspath, logslist[i]))
return settings # Saves the settings
def saveSettings(self, newSettings=None):
if newSettings:
self.settings = newSettings
with open(path.join(self.configFolder, 'config.json'), 'w') as configFile:
json.dump(self.settings, configFile, indent=2)
# Checks if the default settings have changed
def getSettings(): def settingsCheck(self):
global settings
return settings
def getDefaultSettings():
global defaultSettings
return defaultSettings
def saveSettings(newSettings):
global settings
settings = newSettings
with open(path.join(configDir, 'config.json'), 'w') as configFile:
json.dump(settings, configFile, indent=2)
return True
def settingsCheck():
global settings
global defaultSettings
changes = 0 changes = 0
for x in defaultSettings: for x in self.defaultSettings:
if not x in settings or type(settings[x]) != type(defaultSettings[x]): if not x in self.settings or type(self.settings[x]) != type(self.defaultSettings[x]):
settings[x] = defaultSettings[x] self.settings[x] = self.defaultSettings[x]
changes += 1 changes += 1
for x in defaultSettings['tags']: for x in self.defaultSettings['tags']:
if not x in settings['tags'] or type(settings['tags'][x]) != type(defaultSettings['tags'][x]): if not x in self.settings['tags'] or type(self.settings['tags'][x]) != type(self.defaultSettings['tags'][x]):
settings['tags'][x] = defaultSettings['tags'][x] self.settings['tags'][x] = self.defaultSettings['tags'][x]
changes += 1
if self.settings['downloadLocation'] == "":
self.settings['downloadLocation'] = path.join(localpaths.getHomeFolder(), 'deemix Music')
changes += 1
for template in ['tracknameTemplate', 'albumTracknameTemplate', 'playlistTracknameTemplate', 'playlistNameTemplate', 'artistNameTemplate', 'albumNameTemplate', 'playlistFilenameTemplate', 'coverImageTemplate', 'artistImageTemplate']:
if self.settings[template] == "":
self.settings[template] = self.defaultSettings[template]
changes += 1 changes += 1
if changes > 0: if changes > 0:
saveSettings(settings) saveSettings()
def randomString(stringLength=8):
letters = string.ascii_lowercase
return ''.join(random.choice(letters) for i in range(stringLength))

View File

@ -6,18 +6,9 @@ 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, QICollection
emptyPlaylist = {
class SpotifyHelper:
def __init__(self, configFolder=None):
self.credentials = {}
self.spotifyEnabled = False
self.sp = None
if not configFolder:
self.configFolder = getConfigFolder()
else:
self.configFolder = configFolder
self.emptyPlaylist = {
'collaborative': False, 'collaborative': False,
'description': "", 'description': "",
'external_urls': {'spotify': None}, 'external_urls': {'spotify': None},
@ -34,14 +25,26 @@ class SpotifyHelper:
'type': 'playlist', 'type': 'playlist',
'uri': None 'uri': None
} }
self.initCredentials()
def initCredentials(self): class SpotifyHelper:
def __init__(self, configFolder=None):
self.credentials = {}
self.spotifyEnabled = False
self.sp = None
self.configFolder = configFolder
# Make sure config folder exsists
if not self.configFolder:
self.configFolder = getConfigFolder()
if not path.isdir(self.configFolder): if not path.isdir(self.configFolder):
mkdir(self.configFolder) mkdir(self.configFolder)
# Make sure authCredentials exsits
if not path.isfile(path.join(self.configFolder, 'authCredentials.json')): if not path.isfile(path.join(self.configFolder, 'authCredentials.json')):
with open(path.join(self.configFolder, 'authCredentials.json'), 'w') as f: with open(path.join(self.configFolder, 'authCredentials.json'), 'w') as f:
json.dump({'clientId': "", 'clientSecret': ""}, f, indent=2) json.dump({'clientId': "", 'clientSecret': ""}, f, indent=2)
# Load spotify id and secret and check if they are usable
with open(path.join(self.configFolder, 'authCredentials.json'), 'r') as credentialsFile: with open(path.join(self.configFolder, 'authCredentials.json'), 'r') as credentialsFile:
self.credentials = json.load(credentialsFile) self.credentials = json.load(credentialsFile)
self.checkCredentials() self.checkCredentials()
@ -51,7 +54,9 @@ class SpotifyHelper:
spotifyEnabled = False spotifyEnabled = False
else: else:
try: try:
self.createSpotifyConnection() 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.sp.user_playlists('spotify')
self.spotifyEnabled = True self.spotifyEnabled = True
except Exception as e: except Exception as e:
@ -62,18 +67,19 @@ class SpotifyHelper:
return self.credentials return self.credentials
def setCredentials(self, spotifyCredentials): def setCredentials(self, spotifyCredentials):
# Remove extra spaces, just to be sure
spotifyCredentials['clientId'] = spotifyCredentials['clientId'].strip() spotifyCredentials['clientId'] = spotifyCredentials['clientId'].strip()
spotifyCredentials['clientSecret'] = spotifyCredentials['clientSecret'].strip() spotifyCredentials['clientSecret'] = spotifyCredentials['clientSecret'].strip()
# Save them to disk
with open(path.join(self.configFolder, 'authCredentials.json'), 'w') as f: with open(path.join(self.configFolder, 'authCredentials.json'), 'w') as f:
json.dump(spotifyCredentials, f, indent=2) json.dump(spotifyCredentials, f, indent=2)
# Check if they are usable
self.credentials = spotifyCredentials self.credentials = spotifyCredentials
self.checkCredentials() self.checkCredentials()
def createSpotifyConnection(self): # Converts spotify API playlist structure to deezer's playlist structure
client_credentials_manager = SpotifyClientCredentials(client_id=self.credentials['clientId'],
client_secret=self.credentials['clientSecret'])
self.sp = spotipy.Spotify(client_credentials_manager=client_credentials_manager)
def _convert_playlist_structure(self, spotify_obj): def _convert_playlist_structure(self, spotify_obj):
if len(spotify_obj['images']): if len(spotify_obj['images']):
url = spotify_obj['images'][0]['url'] url = spotify_obj['images'][0]['url']
@ -114,6 +120,7 @@ class SpotifyHelper:
deezer_obj['picture_xl'] = "https://e-cdns-images.dzcdn.net/images/cover/d41d8cd98f00b204e9800998ecf8427e/1000x1000-000000-80-0-0.jpg" deezer_obj['picture_xl'] = "https://e-cdns-images.dzcdn.net/images/cover/d41d8cd98f00b204e9800998ecf8427e/1000x1000-000000-80-0-0.jpg"
return deezer_obj return deezer_obj
# Returns deezer song_id from spotify track_id or track dict
def get_trackid_spotify(self, dz, track_id, fallbackSearch, spotifyTrack=None): def get_trackid_spotify(self, dz, track_id, fallbackSearch, spotifyTrack=None):
if not self.spotifyEnabled: if not self.spotifyEnabled:
raise spotifyFeaturesNotEnabled raise spotifyFeaturesNotEnabled
@ -147,6 +154,7 @@ class SpotifyHelper:
json.dump(cache, spotifyCache) json.dump(cache, spotifyCache)
return dz_track return dz_track
# Returns deezer album_id from spotify album_id
def get_albumid_spotify(self, dz, album_id): def get_albumid_spotify(self, dz, album_id):
if not self.spotifyEnabled: if not self.spotifyEnabled:
raise spotifyFeaturesNotEnabled raise spotifyFeaturesNotEnabled
@ -174,50 +182,65 @@ class SpotifyHelper:
json.dump(cache, spotifyCache) json.dump(cache, spotifyCache)
return dz_album return dz_album
def convert_spotify_playlist(self, dz, playlist_id, settings):
def generate_playlist_queueitem(self, dz, playlist_id, bitrate, settings):
if not self.spotifyEnabled: if not self.spotifyEnabled:
raise spotifyFeaturesNotEnabled raise spotifyFeaturesNotEnabled
spotify_playlist = self.sp.playlist(playlist_id) spotify_playlist = self.sp.playlist(playlist_id)
result = {
'title': spotify_playlist['name'],
'artist': spotify_playlist['owner']['display_name'],
'size': spotify_playlist['tracks']['total'],
'downloaded': 0,
'failed': 0,
'progress': 0,
'errors': [],
'type': 'spotify_playlist',
'settings': settings or {},
'id': playlist_id
}
if len(spotify_playlist['images']): if len(spotify_playlist['images']):
result['cover'] = spotify_playlist['images'][0]['url'] cover = spotify_playlist['images'][0]['url']
else: else:
result[ cover = "https://e-cdns-images.dzcdn.net/images/cover/d41d8cd98f00b204e9800998ecf8427e/75x75-000000-80-0-0.jpg"
'cover'] = "https://e-cdns-images.dzcdn.net/images/cover/d41d8cd98f00b204e9800998ecf8427e/75x75-000000-80-0-0.jpg"
playlistAPI = self._convert_playlist_structure(spotify_playlist) playlistAPI = self._convert_playlist_structure(spotify_playlist)
playlistAPI['various_artist'] = dz.get_artist(5080) playlistAPI['various_artist'] = dz.get_artist(5080)
extra = {}
extra['unconverted'] = []
tracklistTmp = spotify_playlist['tracks']['items'] tracklistTmp = spotify_playlist['tracks']['items']
result['collection'] = []
tracklist = []
while spotify_playlist['tracks']['next']: while spotify_playlist['tracks']['next']:
spotify_playlist['tracks'] = self.sp.next(spotify_playlist['tracks']) spotify_playlist['tracks'] = self.sp.next(spotify_playlist['tracks'])
tracklistTmp += spotify_playlist['tracks']['items'] tracklistTmp += spotify_playlist['tracks']['items']
for item in tracklistTmp: for item in tracklistTmp:
if item['track']: if item['track']:
tracklist.append(item['track']) if item['track']['explicit']:
totalSize = len(tracklist) playlistAPI['explicit'] = True
result['size'] = totalSize extra['unconverted'].append(item['track'])
totalSize = len(extra['unconverted'])
if not 'explicit' in playlistAPI:
playlistAPI['explicit'] = False
extra['playlistAPI'] = playlistAPI
return QIConvertable(
playlist_id,
bitrate,
spotify_playlist['name'],
spotify_playlist['owner']['display_name'],
cover,
totalSize,
'spotify_playlist',
settings,
extra,
)
def convert_spotify_playlist(self, dz, queueItem, interface=None):
convertPercentage = 0
lastPercentage = 0
if path.isfile(path.join(self.configFolder, 'spotifyCache.json')): if path.isfile(path.join(self.configFolder, 'spotifyCache.json')):
with open(path.join(self.configFolder, 'spotifyCache.json'), 'r') as spotifyCache: with open(path.join(self.configFolder, 'spotifyCache.json'), 'r') as spotifyCache:
cache = json.load(spotifyCache) cache = json.load(spotifyCache)
else: else:
cache = {'tracks': {}, 'albums': {}} cache = {'tracks': {}, 'albums': {}}
for pos, track in enumerate(tracklist, start=1): if interface:
interface.send("startConversion", queueItem.uuid)
collection = []
for pos, track in enumerate(queueItem.extra['unconverted'], start=1):
if str(track['id']) in cache['tracks']: if str(track['id']) in cache['tracks']:
trackID = cache['tracks'][str(track['id'])] trackID = cache['tracks'][str(track['id'])]
else: else:
trackID = self.get_trackid_spotify(dz, 0, settings['fallbackSearch'], track) trackID = self.get_trackid_spotify(dz, 0, queueItem.settings['fallbackSearch'], track)
cache['tracks'][str(track['id'])] = trackID cache['tracks'][str(track['id'])] = trackID
if trackID == 0: if trackID == 0:
deezerTrack = { deezerTrack = {
@ -234,18 +257,25 @@ class SpotifyHelper:
} }
else: else:
deezerTrack = dz.get_track_gw(trackID) deezerTrack = dz.get_track_gw(trackID)
if 'EXPLICIT_LYRICS' in deezerTrack and deezerTrack['EXPLICIT_LYRICS'] == "1": deezerTrack['_EXTRA_PLAYLIST'] = queueItem.extra['playlistAPI']
playlistAPI['explicit'] = True
deezerTrack['_EXTRA_PLAYLIST'] = playlistAPI
deezerTrack['POSITION'] = pos deezerTrack['POSITION'] = pos
deezerTrack['SIZE'] = totalSize deezerTrack['SIZE'] = queueItem.size
deezerTrack['FILENAME_TEMPLATE'] = settings['playlistTracknameTemplate'] deezerTrack['FILENAME_TEMPLATE'] = queueItem.settings['playlistTracknameTemplate']
result['collection'].append(deezerTrack) collection.append(deezerTrack)
if not 'explicit' in playlistAPI:
playlistAPI['explicit'] = False convertPercentage = (pos / queueItem.size) * 100
if round(convertPercentage) != lastPercentage and round(convertPercentage) % 5 == 0:
lastPercentage = round(convertPercentage)
if interface:
interface.send("updateQueue", {'uuid': queueItem.uuid, 'conversion': lastPercentage})
queueItem.extra = None
queueItem.collection = collection
with open(path.join(self.configFolder, 'spotifyCache.json'), 'w') as spotifyCache: with open(path.join(self.configFolder, 'spotifyCache.json'), 'w') as spotifyCache:
json.dump(cache, spotifyCache) json.dump(cache, spotifyCache)
return result if interface:
interface.send("startDownload", queueItem.uuid)
def get_user_playlists(self, user): def get_user_playlists(self, user):
if not self.spotifyEnabled: if not self.spotifyEnabled:

354
deemix/app/track.py Normal file
View File

@ -0,0 +1,354 @@
#!/usr/bin/env python3
import logging
from deemix.api.deezer import APIError
from deemix.utils.misc import removeFeatures, andCommaConcat, uniqueArray
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger('deemix')
class Track:
def __init__(self, dz, settings, trackAPI_gw, trackAPI=None, albumAPI_gw=None, albumAPI=None):
self.parseEssentialData(dz, trackAPI_gw)
self.title = trackAPI_gw['SNG_TITLE'].strip()
if 'VERSION' in trackAPI_gw and trackAPI_gw['VERSION'] and not trackAPI_gw['VERSION'] in trackAPI_gw['SNG_TITLE']:
self.title += " " + trackAPI_gw['VERSION'].strip()
self.position = None
if 'POSITION' in trackAPI_gw:
self.position = trackAPI_gw['POSITION']
self.localTrack = int(self.id) < 0
if self.localTrack:
self.parseLocalTrackData(trackAPI_gw)
else:
self.parseData(dz, settings, trackAPI_gw, trackAPI, albumAPI_gw, albumAPI)
if not 'Main' in self.artist:
self.artist['Main'] = [self.mainArtist['name']]
# Fix incorrect day month when detectable
if int(self.date['month']) > 12:
monthTemp = self.date['month']
self.date['month'] = self.date['day']
self.date['day'] = monthTemp
if int(self.album['date']['month']) > 12:
monthTemp = self.album['date']['month']
self.album['date']['month'] = self.album['date']['day']
self.album['date']['day'] = monthTemp
# Add playlist data if track is in a playlist
self.playlist = None
if "_EXTRA_PLAYLIST" in trackAPI_gw:
self.playlist = {}
if 'dzcdn.net' in trackAPI_gw["_EXTRA_PLAYLIST"]['picture_small']:
self.playlist['picUrl'] = trackAPI_gw["_EXTRA_PLAYLIST"]['picture_small'][:-24] + "/{}x{}-{}".format(
settings['embeddedArtworkSize'], settings['embeddedArtworkSize'],
f'000000-{settings["jpegImageQuality"]}-0-0.jpg')
else:
self.playlist['picUrl'] = trackAPI_gw["_EXTRA_PLAYLIST"]['picture_xl']
self.playlist['title'] = trackAPI_gw["_EXTRA_PLAYLIST"]['title']
self.playlist['mainArtist'] = {
'id': trackAPI_gw["_EXTRA_PLAYLIST"]['various_artist']['id'],
'name': trackAPI_gw["_EXTRA_PLAYLIST"]['various_artist']['name'],
'pic': trackAPI_gw["_EXTRA_PLAYLIST"]['various_artist']['picture_small'][
trackAPI_gw["_EXTRA_PLAYLIST"]['various_artist']['picture_small'].find('artist/') + 7:-24]
}
if settings['albumVariousArtists']:
self.playlist['artist'] = {"Main": [trackAPI_gw["_EXTRA_PLAYLIST"]['various_artist']['name'], ]}
self.playlist['artists'] = [trackAPI_gw["_EXTRA_PLAYLIST"]['various_artist']['name'], ]
else:
self.playlist['artist'] = {"Main": []}
self.playlist['artists'] = []
self.playlist['trackTotal'] = trackAPI_gw["_EXTRA_PLAYLIST"]['nb_tracks']
self.playlist['recordType'] = "Compilation"
self.playlist['barcode'] = ""
self.playlist['label'] = ""
self.playlist['explicit'] = trackAPI_gw['_EXTRA_PLAYLIST']['explicit']
self.playlist['date'] = {
'day': trackAPI_gw["_EXTRA_PLAYLIST"]["creation_date"][8:10],
'month': trackAPI_gw["_EXTRA_PLAYLIST"]["creation_date"][5:7],
'year': trackAPI_gw["_EXTRA_PLAYLIST"]["creation_date"][0:4]
}
self.playlist['discTotal'] = "1"
self.mainArtistsString = andCommaConcat(self.artist['Main'])
self.featArtistsString = None
if 'Featured' in self.artist:
self.featArtistsString = "feat. "+andCommaConcat(self.artist['Featured'])
# Bits useful for later
self.searched = False
self.selectedFormat = 0
self.dateString = None
self.album['picUrl'] = None
self.album['picPath'] = None
self.album['bitrate'] = 0
self.album['dateString'] = None
self.artistsString = ""
def parseEssentialData(self, dz, trackAPI_gw):
self.id = trackAPI_gw['SNG_ID']
self.duration = trackAPI_gw['DURATION']
self.MD5 = trackAPI_gw['MD5_ORIGIN']
self.mediaVersion = trackAPI_gw['MEDIA_VERSION']
self.fallbackId = "0"
if 'FALLBACK' in trackAPI_gw:
self.fallbackId = trackAPI_gw['FALLBACK']['SNG_ID']
self.filesizes = dz.get_track_filesizes(self.id)
def parseLocalTrackData(self, trackAPI_gw):
self.album = {
'id': "0",
'title': trackAPI_gw['ALB_TITLE'],
'pic': ""
}
if 'ALB_PICTURE' in trackAPI_gw:
self.album['pic'] = trackAPI_gw['ALB_PICTURE']
self.mainArtist = {
'id': "0",
'name': trackAPI_gw['ART_NAME'],
'pic': ""
}
self.artists = [trackAPI_gw['ART_NAME']]
self.artist = {
'Main': [trackAPI_gw['ART_NAME']]
}
self.date = {
'day': "00",
'month': "00",
'year': "XXXX"
}
# All the missing data
self.ISRC = ""
self.album['artist'] = self.artist
self.album['artists'] = self.artists
self.album['barcode'] = "Unknown"
self.album['date'] = self.date
self.album['discTotal'] = "0"
self.album['explicit'] = False
self.album['genre'] = []
self.album['label'] = "Unknown"
self.album['mainArtist'] = self.mainArtist
self.album['recordType'] = "Album"
self.album['trackTotal'] = "0"
self.bpm = 0
self.contributors = {}
self.copyright = ""
self.discNumber = "0"
self.explicit = False
self.lyrics = {}
self.replayGain = ""
self.trackNumber = "0"
def parseData(self, dz, settings, trackAPI_gw, trackAPI, albumAPI_gw, albumAPI):
self.discNumber = None
if 'DISK_NUMBER' in trackAPI_gw:
self.discNumber = trackAPI_gw['DISK_NUMBER']
self.explicit = None
if 'EXPLICIT_LYRICS' in trackAPI_gw:
self.explicit = trackAPI_gw['EXPLICIT_LYRICS']
self.copyright = None
if 'COPYRIGHT' in trackAPI_gw:
self.copyright = trackAPI_gw['COPYRIGHT']
self.replayGain = ""
if 'GAIN' in trackAPI_gw:
self.replayGain = "{0:.2f} dB".format((float(trackAPI_gw['GAIN']) + 18.4) * -1)
self.ISRC = trackAPI_gw['ISRC']
self.trackNumber = trackAPI_gw['TRACK_NUMBER']
self.contributors = trackAPI_gw['SNG_CONTRIBUTORS']
self.lyrics = {
'id': None,
'unsync': None,
'sync': None
}
if 'LYRICS_ID' in trackAPI_gw:
self.lyrics['id'] = trackAPI_gw['LYRICS_ID']
if not "LYRICS" in trackAPI_gw and int(self.lyrics['id']) != 0:
logger.info(f"[{trackAPI_gw['ART_NAME']} - {self.title}] Getting lyrics")
trackAPI_gw["LYRICS"] = dz.get_lyrics_gw(self.id)
if int(self.lyrics['id']) != 0:
if "LYRICS_TEXT" in trackAPI_gw["LYRICS"]:
self.lyrics['unsync'] = trackAPI_gw["LYRICS"]["LYRICS_TEXT"]
if "LYRICS_SYNC_JSON" in trackAPI_gw["LYRICS"]:
self.lyrics['sync'] = ""
lastTimestamp = ""
for i in range(len(trackAPI_gw["LYRICS"]["LYRICS_SYNC_JSON"])):
if "lrc_timestamp" in trackAPI_gw["LYRICS"]["LYRICS_SYNC_JSON"][i]:
self.lyrics['sync'] += trackAPI_gw["LYRICS"]["LYRICS_SYNC_JSON"][i]["lrc_timestamp"]
lastTimestamp = trackAPI_gw["LYRICS"]["LYRICS_SYNC_JSON"][i]["lrc_timestamp"]
else:
self.lyrics['sync'] += lastTimestamp
self.lyrics['sync'] += trackAPI_gw["LYRICS"]["LYRICS_SYNC_JSON"][i]["line"] + "\r\n"
self.mainArtist = {
'id': trackAPI_gw['ART_ID'],
'name': trackAPI_gw['ART_NAME'],
'pic': None
}
if 'ART_PICTURE' in trackAPI_gw:
self.mainArtist['pic'] = trackAPI_gw['ART_PICTURE']
self.date = None
if 'PHYSICAL_RELEASE_DATE' in trackAPI_gw:
self.date = {
'day': trackAPI_gw["PHYSICAL_RELEASE_DATE"][8:10],
'month': trackAPI_gw["PHYSICAL_RELEASE_DATE"][5:7],
'year': trackAPI_gw["PHYSICAL_RELEASE_DATE"][0:4]
}
self.album = {
'id': trackAPI_gw['ALB_ID'],
'title': trackAPI_gw['ALB_TITLE'],
'pic': None,
'barcode': "Unknown",
'label': "Unknown",
'explicit': False,
'date': None,
'genre': []
}
if 'ALB_PICTURE' in trackAPI_gw:
self.album['pic'] = trackAPI_gw['ALB_PICTURE']
try:
# Try the public API first (as it has more data)
if not albumAPI:
logger.info(f"[{self.mainArtist['name']} - {self.title}] Getting album infos")
albumAPI = dz.get_album(self.album['id'])
self.album['title'] = albumAPI['title']
self.album['mainArtist'] = {
'id': albumAPI['artist']['id'],
'name': albumAPI['artist']['name'],
'pic': albumAPI['artist']['picture_small'][albumAPI['artist']['picture_small'].find('artist/') + 7:-24]
}
self.album['artist'] = {}
self.album['artists'] = []
for artist in albumAPI['contributors']:
if artist['id'] != 5080 or artist['id'] == 5080 and settings['albumVariousArtists']:
if artist['name'] not in self.album['artists']:
self.album['artists'].append(artist['name'])
if artist['role'] == "Main" or artist['role'] != "Main" and artist['name'] not in self.album['artist']['Main']:
if not artist['role'] in self.album['artist']:
self.album['artist'][artist['role']] = []
self.album['artist'][artist['role']].append(artist['name'])
if settings['removeDuplicateArtists']:
self.album['artists'] = uniqueArray(self.album['artists'])
for role in self.album['artist'].keys():
self.album['artist'][role] = uniqueArray(self.album['artist'][role])
self.album['trackTotal'] = albumAPI['nb_tracks']
self.album['recordType'] = albumAPI['record_type']
if 'upc' in albumAPI:
self.album['barcode'] = albumAPI['upc']
if 'label' in albumAPI:
self.album['label'] = albumAPI['label']
if 'explicit_lyrics' in albumAPI:
self.album['explicit'] = albumAPI['explicit_lyrics']
if 'release_date' in albumAPI:
self.album['date'] = {
'day': albumAPI["release_date"][8:10],
'month': albumAPI["release_date"][5:7],
'year': albumAPI["release_date"][0:4]
}
self.album['discTotal'] = None
if 'nb_disk' in albumAPI:
self.album['discTotal'] = albumAPI['nb_disk']
self.copyright = None
if 'copyright' in albumAPI:
self.copyright = albumAPI['copyright']
if not self.album['pic']:
self.album['pic'] = albumAPI['cover_small'][albumAPI['cover_small'].find('cover/') + 6:-24]
if 'genres' in albumAPI and 'data' in albumAPI['genres'] and len(albumAPI['genres']['data']) > 0:
for genre in albumAPI['genres']['data']:
self.album['genre'].append(genre['name'])
except APIError:
if not albumAPI_gw:
logger.info(f"[{self.mainArtist['name']} - {self.title}] Getting more album infos")
albumAPI_gw = dz.get_album_gw(self.album['id'])
self.album['title'] = albumAPI_gw['ALB_TITLE']
self.album['mainArtist'] = {
'id': albumAPI_gw['ART_ID'],
'name': albumAPI_gw['ART_NAME'],
'pic': None
}
logger.info(f"[{self.mainArtist['name']} - {self.title}] Getting artist picture fallback")
artistAPI = dz.get_artist(self.album['mainArtist']['id'])
self.album['artists'] = [albumAPI_gw['ART_NAME']]
self.album['mainArtist']['pic'] = artistAPI['picture_small'][artistAPI['picture_small'].find('artist/') + 7:-24]
self.album['trackTotal'] = albumAPI_gw['NUMBER_TRACK']
self.album['discTotal'] = albumAPI_gw['NUMBER_DISK']
self.album['recordType'] = "Album"
if 'LABEL_NAME' in albumAPI_gw:
self.album['label'] = albumAPI_gw['LABEL_NAME']
if 'EXPLICIT_ALBUM_CONTENT' in albumAPI_gw and 'EXPLICIT_LYRICS_STATUS' in albumAPI_gw['EXPLICIT_ALBUM_CONTENT']:
self.album['explicit'] = albumAPI_gw['EXPLICIT_ALBUM_CONTENT']['EXPLICIT_LYRICS_STATUS'] in [1,4]
if not self.album['pic']:
self.album['pic'] = albumAPI_gw['ALB_PICTURE']
if 'PHYSICAL_RELEASE_DATE' in albumAPI_gw:
self.album['date'] = {
'day': albumAPI_gw["PHYSICAL_RELEASE_DATE"][8:10],
'month': albumAPI_gw["PHYSICAL_RELEASE_DATE"][5:7],
'year': albumAPI_gw["PHYSICAL_RELEASE_DATE"][0:4]
}
self.album['mainArtist']['save'] = self.album['mainArtist']['id'] != 5080 or self.album['mainArtist']['id'] == 5080 and settings['albumVariousArtists']
if self.album['date'] and not self.date:
self.date = self.album['date']
if not trackAPI:
logger.info(f"[{self.mainArtist['name']} - {self.title}] Getting extra track infos")
trackAPI = dz.get_track(self.id)
self.bpm = trackAPI['bpm']
if not self.replayGain and 'gain' in trackAPI:
self.replayGain = "{0:.2f} dB".format((float(trackAPI['gain']) + 18.4) * -1)
if not self.explicit:
self.explicit = trackAPI['explicit_lyrics']
if not self.discNumber:
self.discNumber = trackAPI['disk_number']
self.artist = {}
self.artists = []
for artist in trackAPI['contributors']:
if artist['id'] != 5080 or artist['id'] == 5080 and len(trackAPI['contributors']) == 1:
if artist['name'] not in self.artists:
self.artists.append(artist['name'])
if artist['role'] != "Main" and artist['name'] not in self.artist['Main'] or artist['role'] == "Main":
if not artist['role'] in self.artist:
self.artist[artist['role']] = []
self.artist[artist['role']].append(artist['name'])
if settings['removeDuplicateArtists']:
self.artists = uniqueArray(self.artists)
for role in self.artist.keys():
self.artist[role] = uniqueArray(self.artist[role])
if not self.album['discTotal']:
if not albumAPI_gw:
logger.info(f"[{self.mainArtist['name']} - {self.title}] Getting more album infos")
albumAPI_gw = dz.get_album_gw(self.album['id'])
self.album['discTotal'] = albumAPI_gw['NUMBER_DISK']
if not self.copyright:
if not albumAPI_gw:
logger.info(f"[{self.mainArtist['name']} - {self.title}] Getting more album infos")
albumAPI_gw = dz.get_album_gw(self.album['id'])
self.copyright = albumAPI_gw['COPYRIGHT']
# Removes featuring from the title
def getCleanTitle(self):
return removeFeatures(self.title)
# Removes featuring from the album name
def getCleanAlbumTitle(self):
return removeFeatures(self.album['title'])
def getFeatTitle(self):
if self.featArtistsString and not "(feat." in self.title.lower():
return self.title + " ({})".format(self.featArtistsString)
return self.title

View File

@ -11,14 +11,12 @@ if getenv("APPDATA"):
elif sys.platform.startswith('darwin'): elif sys.platform.startswith('darwin'):
userdata = homedata + '/Library/Application Support/deemix/' userdata = homedata + '/Library/Application Support/deemix/'
elif getenv("XDG_CONFIG_HOME"): elif getenv("XDG_CONFIG_HOME"):
userdata = getenv("XDG_CONFIG_HOME") + '/deemix/'; userdata = getenv("XDG_CONFIG_HOME") + '/deemix/'
else: else:
userdata = homedata + '/.config/deemix/'; userdata = homedata + '/.config/deemix/'
def getHomeFolder(): def getHomeFolder():
return homedata return homedata
def getConfigFolder(): def getConfigFolder():
return userdata return userdata

View File

@ -34,6 +34,30 @@ def changeCase(str, type):
return str return str
def removeFeatures(title):
clean = title
if "(feat." in clean.lower():
pos = clean.lower().find("(feat.")
tempTrack = clean[:pos]
if ")" in clean:
tempTrack += clean[clean.find(")", pos + 1) + 1:]
clean = tempTrack.strip()
return clean
def andCommaConcat(lst):
tot = len(lst)
result = ""
for i, art in enumerate(lst):
result += art
if tot != i + 1:
if tot - 1 == i + 1:
result += " & "
else:
result += ", "
return result
def getIDFromLink(link, type): def getIDFromLink(link, type):
if '?' in link: if '?' in link:
link = link[:link.find('?')] link = link[:link.find('?')]
@ -94,12 +118,3 @@ def uniqueArray(arr):
if iPrinc!=iRest and namePrinc.lower() in nRest.lower(): if iPrinc!=iRest and namePrinc.lower() in nRest.lower():
del arr[iRest] del arr[iRest]
return arr return arr
def isValidLink(text):
if text.lower().startswith("http"):
if "deezer.com" in text.lower() or "open.spotify.com" in text.lower():
return True
elif text.lower().startswith("spotify:"):
return True
return False

View File

@ -94,10 +94,10 @@ def generateFilepath(track, trackAPI, settings):
'savePlaylistAsCompilation']) or 'savePlaylistAsCompilation']) or
(settings['createArtistFolder'] and '_EXTRA_PLAYLIST' in trackAPI and settings['createStructurePlaylist']) (settings['createArtistFolder'] and '_EXTRA_PLAYLIST' in trackAPI and settings['createStructurePlaylist'])
): ):
if (int(track['id']) < 0 and not 'mainArtist' in track['album']): if (int(track.id) < 0 and not 'mainArtist' in track.album):
track['album']['mainArtist'] = track['mainArtist'] track.album['mainArtist'] = track.mainArtist
filepath += antiDot( filepath += antiDot(
settingsRegexArtist(settings['artistNameTemplate'], track['album']['mainArtist'], settings)) + pathSep settingsRegexArtist(settings['artistNameTemplate'], track.album['mainArtist'], settings)) + pathSep
artistPath = filepath artistPath = filepath
if (settings['createAlbumFolder'] and if (settings['createAlbumFolder'] and
@ -107,7 +107,7 @@ def generateFilepath(track, trackAPI, settings):
'_EXTRA_PLAYLIST' in trackAPI and settings['createStructurePlaylist'])) '_EXTRA_PLAYLIST' in trackAPI and settings['createStructurePlaylist']))
): ):
filepath += antiDot( filepath += antiDot(
settingsRegexAlbum(settings['albumNameTemplate'], track['album'], settings, settingsRegexAlbum(settings['albumNameTemplate'], track.album, settings,
trackAPI['_EXTRA_PLAYLIST'] if'_EXTRA_PLAYLIST' in trackAPI else None)) + pathSep trackAPI['_EXTRA_PLAYLIST'] if'_EXTRA_PLAYLIST' in trackAPI else None)) + pathSep
coverPath = filepath coverPath = filepath
@ -115,55 +115,55 @@ def generateFilepath(track, trackAPI, settings):
extrasPath = filepath extrasPath = filepath
if ( if (
int(track['album']['discTotal']) > 1 and ( int(track.album['discTotal']) > 1 and (
(settings['createAlbumFolder'] and settings['createCDFolder']) and (settings['createAlbumFolder'] and settings['createCDFolder']) and
(not 'SINGLE_TRACK' in trackAPI or ('SINGLE_TRACK' in trackAPI and settings['createSingleFolder'])) and (not 'SINGLE_TRACK' in trackAPI or ('SINGLE_TRACK' in trackAPI and settings['createSingleFolder'])) and
(not '_EXTRA_PLAYLIST' in trackAPI or ( (not '_EXTRA_PLAYLIST' in trackAPI or (
'_EXTRA_PLAYLIST' in trackAPI and settings['tags']['savePlaylistAsCompilation']) or ( '_EXTRA_PLAYLIST' in trackAPI and settings['tags']['savePlaylistAsCompilation']) or (
'_EXTRA_PLAYLIST' in trackAPI and settings['createStructurePlaylist'])) '_EXTRA_PLAYLIST' in trackAPI and settings['createStructurePlaylist']))
)): )):
filepath += 'CD' + str(track['discNumber']) + pathSep filepath += 'CD' + str(track.discNumber) + pathSep
return (filepath, artistPath, coverPath, extrasPath) return (filepath, artistPath, coverPath, extrasPath)
def settingsRegex(filename, track, settings, playlist=None): def settingsRegex(filename, track, settings, playlist=None):
filename = filename.replace("%title%", fixName(track['title'], settings['illegalCharacterReplacer'])) filename = filename.replace("%title%", fixName(track.title, settings['illegalCharacterReplacer']))
filename = filename.replace("%artist%", fixName(track['mainArtist']['name'], settings['illegalCharacterReplacer'])) filename = filename.replace("%artist%", fixName(track.mainArtist['name'], settings['illegalCharacterReplacer']))
filename = filename.replace("%artists%", fixName(track['commaArtistsString'], settings['illegalCharacterReplacer'])) filename = filename.replace("%artists%", fixName(", ".join(track.artists), settings['illegalCharacterReplacer']))
filename = filename.replace("%allartists%", fixName(track['artistsString'], settings['illegalCharacterReplacer'])) filename = filename.replace("%allartists%", fixName(track.artistsString, settings['illegalCharacterReplacer']))
filename = filename.replace("%mainartists%", fixName(track['mainArtistsString'], settings['illegalCharacterReplacer'])) filename = filename.replace("%mainartists%", fixName(track.mainArtistsString, settings['illegalCharacterReplacer']))
filename = filename.replace("%featartists%", fixName('('+track['featArtistsString']+')', settings['illegalCharacterReplacer']) if 'featArtistsString' in track else "") filename = filename.replace("%featartists%", fixName('('+track.featArtistsString+')', settings['illegalCharacterReplacer']) if track.featArtistsString else "")
filename = filename.replace("%album%", fixName(track['album']['title'], settings['illegalCharacterReplacer'])) filename = filename.replace("%album%", fixName(track.album['title'], settings['illegalCharacterReplacer']))
filename = filename.replace("%albumartist%", filename = filename.replace("%albumartist%",
fixName(track['album']['mainArtist']['name'], settings['illegalCharacterReplacer'])) fixName(track.album['mainArtist']['name'], settings['illegalCharacterReplacer']))
filename = filename.replace("%tracknumber%", pad(track['trackNumber'], track['album']['trackTotal'] if int( filename = filename.replace("%tracknumber%", pad(track.trackNumber, track.album['trackTotal'] if int(
settings['paddingSize']) == 0 else 10 ** (int(settings['paddingSize']) - 1), settings['padTracks'])) settings['paddingSize']) == 0 else 10 ** (int(settings['paddingSize']) - 1), settings['padTracks']))
filename = filename.replace("%tracktotal%", str(track['album']['trackTotal'])) filename = filename.replace("%tracktotal%", str(track.album['trackTotal']))
filename = filename.replace("%discnumber%", str(track['discNumber'])) filename = filename.replace("%discnumber%", str(track.discNumber))
filename = filename.replace("%disctotal%", str(track['album']['discTotal'])) filename = filename.replace("%disctotal%", str(track.album['discTotal']))
if len(track['album']['genre']) > 0: if len(track.album['genre']) > 0:
filename = filename.replace("%genre%", filename = filename.replace("%genre%",
fixName(track['album']['genre'][0], settings['illegalCharacterReplacer'])) fixName(track.album['genre'][0], settings['illegalCharacterReplacer']))
else: else:
filename = filename.replace("%genre%", "Unknown") filename = filename.replace("%genre%", "Unknown")
filename = filename.replace("%year%", str(track['date']['year'])) filename = filename.replace("%year%", str(track.date['year']))
filename = filename.replace("%date%", track['dateString']) filename = filename.replace("%date%", track.dateString)
filename = filename.replace("%bpm%", str(track['bpm'])) filename = filename.replace("%bpm%", str(track.bpm))
filename = filename.replace("%label%", fixName(track['album']['label'], settings['illegalCharacterReplacer'])) filename = filename.replace("%label%", fixName(track.album['label'], settings['illegalCharacterReplacer']))
filename = filename.replace("%isrc%", track['ISRC']) filename = filename.replace("%isrc%", track.ISRC)
filename = filename.replace("%upc%", track['album']['barcode']) filename = filename.replace("%upc%", track.album['barcode'])
filename = filename.replace("%explicit%", "(Explicit)" if track['explicit'] else "") filename = filename.replace("%explicit%", "(Explicit)" if track.explicit else "")
filename = filename.replace("%track_id%", str(track['id'])) filename = filename.replace("%track_id%", str(track.id))
filename = filename.replace("%album_id%", str(track['album']['id'])) filename = filename.replace("%album_id%", str(track.album['id']))
filename = filename.replace("%artist_id%", str(track['mainArtist']['id'])) filename = filename.replace("%artist_id%", str(track.mainArtist['id']))
if playlist: if playlist:
filename = filename.replace("%playlist_id%", str(playlist['id'])) filename = filename.replace("%playlist_id%", str(playlist['id']))
filename = filename.replace("%position%", pad(track['position'], playlist['nb_tracks'] if int( filename = filename.replace("%position%", pad(track.position, playlist['nb_tracks'] if int(
settings['paddingSize']) == 0 else 10 ** (int(settings['paddingSize']) - 1), settings['padTracks'])) settings['paddingSize']) == 0 else 10 ** (int(settings['paddingSize']) - 1), settings['padTracks']))
else: else:
filename = filename.replace("%position%", pad(track['trackNumber'], track['album']['trackTotal'] if int( filename = filename.replace("%position%", pad(track.trackNumber, track.album['trackTotal'] if int(
settings['paddingSize']) == 0 else 10 ** (int(settings['paddingSize']) - 1), settings['padTracks'])) settings['paddingSize']) == 0 else 10 ** (int(settings['paddingSize']) - 1), settings['padTracks']))
filename = filename.replace('\\', pathSep).replace('/', pathSep) filename = filename.replace('\\', pathSep).replace('/', pathSep)
return antiDot(fixLongName(filename)) return antiDot(fixLongName(filename))
@ -218,11 +218,11 @@ def settingsRegexPlaylist(foldername, playlist, settings):
return antiDot(fixLongName(foldername)) return antiDot(fixLongName(foldername))
def settingsRegexPlaylistFile(foldername, queueItem, settings): def settingsRegexPlaylistFile(foldername, queueItem, settings):
foldername = foldername.replace("%title%", fixName(queueItem['title'], settings['illegalCharacterReplacer'])) foldername = foldername.replace("%title%", fixName(queueItem.title, settings['illegalCharacterReplacer']))
foldername = foldername.replace("%artist%", fixName(queueItem['artist'], settings['illegalCharacterReplacer'])) foldername = foldername.replace("%artist%", fixName(queueItem.artist, settings['illegalCharacterReplacer']))
foldername = foldername.replace("%size%", str(queueItem['size'])) foldername = foldername.replace("%size%", str(queueItem.size))
foldername = foldername.replace("%type%", fixName(queueItem['type'], settings['illegalCharacterReplacer'])) foldername = foldername.replace("%type%", fixName(queueItem.type, settings['illegalCharacterReplacer']))
foldername = foldername.replace("%id%", fixName(queueItem['id'], settings['illegalCharacterReplacer'])) foldername = foldername.replace("%id%", fixName(queueItem.id, settings['illegalCharacterReplacer']))
foldername = foldername.replace("%bitrate%", bitrateLabels[int(queueItem['bitrate'])]) foldername = foldername.replace("%bitrate%", bitrateLabels[int(queueItem.bitrate)])
foldername = foldername.replace('\\', pathSep).replace('/', pathSep).replace(pathSep, settings['illegalCharacterReplacer']) foldername = foldername.replace('\\', pathSep).replace('/', pathSep).replace(pathSep, settings['illegalCharacterReplacer'])
return antiDot(fixLongName(foldername)) return antiDot(fixLongName(foldername))

View File

@ -3,8 +3,9 @@ from mutagen.flac import FLAC, Picture
from mutagen.id3 import ID3, ID3NoHeaderError, TXXX, TIT2, TPE1, TALB, TPE2, TRCK, TPOS, TCON, TYER, TDAT, TLEN, TBPM, \ from mutagen.id3 import ID3, ID3NoHeaderError, TXXX, TIT2, TPE1, TALB, TPE2, TRCK, TPOS, TCON, TYER, TDAT, TLEN, TBPM, \
TPUB, TSRC, USLT, APIC, IPLS, TCOM, TCOP, TCMP TPUB, TSRC, USLT, APIC, IPLS, TCOM, TCOP, TCMP
# Adds tags to a MP3 file
def tagID3(stream, track, save): def tagID3(stream, track, save):
# Delete exsisting tags
try: try:
tag = ID3(stream) tag = ID3(stream)
tag.delete() tag.delete()
@ -12,141 +13,156 @@ def tagID3(stream, track, save):
tag = ID3() tag = ID3()
if save['title']: if save['title']:
tag.add(TIT2(text=track['title'])) tag.add(TIT2(text=track.title))
if save['artist'] and len(track['artists']):
if save['artist'] and len(track.artists):
if save['multiArtistSeparator'] != "default": if save['multiArtistSeparator'] != "default":
if save['multiArtistSeparator'] == "nothing": if save['multiArtistSeparator'] == "nothing":
tag.add(TPE1(text=track['mainArtist']['name'])) tag.add(TPE1(text=track.mainArtist['name']))
else: else:
tag.add(TPE1(text=track['artistsString'])) tag.add(TPE1(text=track.artistsString))
tag.add(TXXX(desc="ARTISTS", text=track['artists'])) tag.add(TXXX(desc="ARTISTS", text=track.artists))
else: else:
tag.add(TPE1(text=track['artists'])) tag.add(TPE1(text=track.artists))
if save['album']: if save['album']:
tag.add(TALB(text=track['album']['title'])) tag.add(TALB(text=track.album['title']))
if save['albumArtist'] and len(track['album']['artists']):
if save['singleAlbumArtist'] and track['album']['mainArtist']['save']: if save['albumArtist'] and len(track.album['artists']):
tag.add(TPE2(text=track['album']['mainArtist']['name'])) if save['singleAlbumArtist'] and track.album['mainArtist']['save']:
tag.add(TPE2(text=track.album['mainArtist']['name']))
else: else:
tag.add(TPE2(text=track['album']['artists'])) tag.add(TPE2(text=track.album['artists']))
if save['trackNumber']: if save['trackNumber']:
tag.add(TRCK( tag.add(TRCK(
text=str(track['trackNumber']) + ("/" + str(track['album']['trackTotal']) if save['trackTotal'] else ""))) text=str(track.trackNumber) + ("/" + str(track.album['trackTotal']) if save['trackTotal'] else "")))
if save['discNumber']: if save['discNumber']:
tag.add( tag.add(
TPOS(text=str(track['discNumber']) + ("/" + str(track['album']['discTotal']) if save['discTotal'] else ""))) TPOS(text=str(track.discNumber) + ("/" + str(track.album['discTotal']) if save['discTotal'] else "")))
if save['genre']: if save['genre']:
tag.add(TCON(text=track['album']['genre'])) tag.add(TCON(text=track.album['genre']))
if save['year']: if save['year']:
tag.add(TYER(text=str(track['date']['year']))) tag.add(TYER(text=str(track.date['year'])))
if save['date']: if save['date']:
tag.add(TDAT(text=str(track['date']['month']) + str(track['date']['day']))) tag.add(TDAT(text=str(track.date['month']) + str(track.date['day'])))
if save['length']: if save['length']:
tag.add(TLEN(text=str(int(track['duration'])*1000))) tag.add(TLEN(text=str(int(track.duration)*1000)))
if save['bpm']: if save['bpm']:
tag.add(TBPM(text=str(track['bpm']))) tag.add(TBPM(text=str(track.bpm)))
if save['label']: if save['label']:
tag.add(TPUB(text=track['album']['label'])) tag.add(TPUB(text=track.album['label']))
if save['isrc']: if save['isrc']:
tag.add(TSRC(text=track['ISRC'])) tag.add(TSRC(text=track.ISRC))
if save['barcode']: if save['barcode']:
tag.add(TXXX(desc="BARCODE", text=track['album']['barcode'])) tag.add(TXXX(desc="BARCODE", text=track.album['barcode']))
if save['explicit']: if save['explicit']:
tag.add(TXXX(desc="ITUNESADVISORY", text="1" if track['explicit'] else "0")) tag.add(TXXX(desc="ITUNESADVISORY", text="1" if track.explicit else "0"))
if save['replayGain']: if save['replayGain']:
tag.add(TXXX(desc="REPLAYGAIN_TRACK_GAIN", text=track['replayGain'])) tag.add(TXXX(desc="REPLAYGAIN_TRACK_GAIN", text=track.replayGain))
if 'unsync' in track['lyrics'] and save['lyrics']: if 'unsync' in track.lyrics and save['lyrics']:
tag.add(USLT(text=track['lyrics']['unsync'])) tag.add(USLT(text=track.lyrics['unsync']))
involved_people = [] involved_people = []
for role in track['contributors']: for role in track.contributors:
if role in ['author', 'engineer', 'mixer', 'producer', 'writer']: if role in ['author', 'engineer', 'mixer', 'producer', 'writer']:
for person in track['contributors'][role]: for person in track.contributors[role]:
involved_people.append([role, person]) involved_people.append([role, person])
elif role == 'composer' and save['composer']: elif role == 'composer' and save['composer']:
tag.add(TCOM(text=track['contributors']['composer'])) tag.add(TCOM(text=track.contributors['composer']))
if len(involved_people) > 0 and save['involvedPeople']: if len(involved_people) > 0 and save['involvedPeople']:
tag.add(IPLS(people=involved_people)) tag.add(IPLS(people=involved_people))
if save['copyright']: if save['copyright']:
tag.add(TCOP(text=track['copyright'])) tag.add(TCOP(text=track.copyright))
if save['savePlaylistAsCompilation'] and "playlist" in track: if save['savePlaylistAsCompilation'] and track.playlist:
tag.add(TCMP(text="1")) tag.add(TCMP(text="1"))
if save['cover'] and track['album']['picPath']:
with open(track['album']['picPath'], 'rb') as f: if save['cover'] and track.album['picPath']:
with open(track.album['picPath'], 'rb') as f:
tag.add( tag.add(
APIC(3, 'image/jpeg' if track['album']['picPath'].endswith('jpg') else 'image/png', 3, desc='cover', data=f.read())) APIC(3, 'image/jpeg' if track.album['picPath'].endswith('jpg') else 'image/png', 3, desc='cover', data=f.read()))
tag.save(stream, v1=2 if save['saveID3v1'] else 0, v2_version=3, tag.save(stream, v1=2 if save['saveID3v1'] else 0, v2_version=3,
v23_sep=None if save['useNullSeparator'] else '/') v23_sep=None if save['useNullSeparator'] else '/')
# Adds tags to a FLAC file
def tagFLAC(stream, track, save): def tagFLAC(stream, track, save):
# Delete exsisting tags
tag = FLAC(stream) tag = FLAC(stream)
tag.delete() tag.delete()
tag.clear_pictures() tag.clear_pictures()
if save['title']: if save['title']:
tag["TITLE"] = track['title'] tag["TITLE"] = track.title
if save['artist'] and len(track['artists']):
if save['artist'] and len(track.artists):
if save['multiArtistSeparator'] != "default": if save['multiArtistSeparator'] != "default":
if save['multiArtistSeparator'] == "nothing": if save['multiArtistSeparator'] == "nothing":
tag["ARTIST"] = track['mainArtist']['name'] tag["ARTIST"] = track.mainArtist['name']
else: else:
tag["ARTIST"] = track['artistsString'] tag["ARTIST"] = track.artistsString
tag["ARTISTS"] = track['artists'] tag["ARTISTS"] = track.artists
else: else:
tag["ARTIST"] = track['artists'] tag["ARTIST"] = track.artists
if save['album']: if save['album']:
tag["ALBUM"] = track['album']['title'] tag["ALBUM"] = track.album['title']
if save['albumArtist'] and len(track['album']['artists']):
if save['albumArtist'] and len(track.album['artists']):
if save['singleAlbumArtist']: if save['singleAlbumArtist']:
tag["ALBUMARTIST"] = track['album']['mainArtist']['name'] tag["ALBUMARTIST"] = track.album['mainArtist']['name']
else: else:
tag["ALBUMARTIST"] = track['album']['artists'] tag["ALBUMARTIST"] = track.album['artists']
if save['trackNumber']: if save['trackNumber']:
tag["TRACKNUMBER"] = str(track['trackNumber']) tag["TRACKNUMBER"] = str(track.trackNumber)
if save['trackTotal']: if save['trackTotal']:
tag["TRACKTOTAL"] = str(track['album']['trackTotal']) tag["TRACKTOTAL"] = str(track.album['trackTotal'])
if save['discNumber']: if save['discNumber']:
tag["DISCNUMBER"] = str(track['discNumber']) tag["DISCNUMBER"] = str(track.discNumber)
if save['discTotal']: if save['discTotal']:
tag["DISCTOTAL"] = str(track['album']['discTotal']) tag["DISCTOTAL"] = str(track.album['discTotal'])
if save['genre']: if save['genre']:
tag["GENRE"] = track['album']['genre'] tag["GENRE"] = track.album['genre']
if save['date']: if save['date']:
tag["DATE"] = track['dateString'] tag["DATE"] = track.dateString
elif save['year']: elif save['year']:
tag["YEAR"] = str(track['date']['year']) tag["YEAR"] = str(track.date['year'])
if save['length']: if save['length']:
tag["LENGTH"] = str(track['duration']) tag["LENGTH"] = str(track.duration)
if save['bpm']: if save['bpm']:
tag["BPM"] = str(track['bpm']) tag["BPM"] = str(track.bpm)
if save['label']: if save['label']:
tag["PUBLISHER"] = track['album']['label'] tag["PUBLISHER"] = track.album['label']
if save['isrc']: if save['isrc']:
tag["ISRC"] = track['ISRC'] tag["ISRC"] = track.ISRC
if save['barcode']: if save['barcode']:
tag["BARCODE"] = track['album']['barcode'] tag["BARCODE"] = track.album['barcode']
if save['explicit']: if save['explicit']:
tag["ITUNESADVISORY"] = "1" if track['explicit'] else "0" tag["ITUNESADVISORY"] = "1" if track.explicit else "0"
if save['replayGain']: if save['replayGain']:
tag["REPLAYGAIN_TRACK_GAIN"] = track['replayGain'] tag["REPLAYGAIN_TRACK_GAIN"] = track.replayGain
if 'unsync' in track['lyrics'] and save['lyrics']: if 'unsync' in track.lyrics and save['lyrics']:
tag["LYRICS"] = track['lyrics']['unsync'] tag["LYRICS"] = track.lyrics['unsync']
for role in track['contributors']:
for role in track.contributors:
if role in ['author', 'engineer', 'mixer', 'producer', 'writer', 'composer']: if role in ['author', 'engineer', 'mixer', 'producer', 'writer', 'composer']:
if save['involvedPeople'] and role != 'composer' or role == 'composer' and save['composer']: if save['involvedPeople'] and role != 'composer' or role == 'composer' and save['composer']:
tag[role] = track['contributors'][role] tag[role] = track.contributors[role]
elif role == 'musicpublisher' and save['involvedPeople']: elif role == 'musicpublisher' and save['involvedPeople']:
tag["ORGANIZATION"] = track['contributors']['musicpublisher'] tag["ORGANIZATION"] = track.contributors['musicpublisher']
if save['copyright']: if save['copyright']:
tag["COPYRIGHT"] = track['copyright'] tag["COPYRIGHT"] = track.copyright
if save['savePlaylistAsCompilation'] and "playlist" in track: if save['savePlaylistAsCompilation'] and track.playlist:
tag["COMPILATION"] = "1" tag["COMPILATION"] = "1"
if save['cover'] and track['album']['picPath']: if save['cover'] and track.album['picPath']:
image = Picture() image = Picture()
image.type = 3 image.type = 3
image.mime = 'image/jpeg' if track['album']['picPath'].endswith('jpg') else 'image/png' image.mime = 'image/jpeg' if track.album['picPath'].endswith('jpg') else 'image/png'
with open(track['album']['picPath'], 'rb') as f: with open(track.album['picPath'], 'rb') as f:
image.data = f.read() image.data = f.read()
tag.add_picture(image) tag.add_picture(image)

View File

@ -7,7 +7,7 @@ README = (HERE / "README.md").read_text()
setup( setup(
name="deemix", name="deemix",
version="1.1.31", version="1.2.0",
description="A barebone deezer downloader library", description="A barebone deezer downloader library",
long_description=README, long_description=README,
long_description_content_type="text/markdown", long_description_content_type="text/markdown",