Merge refactoring (#4)
Removed saveDownloadQueue and tagsLanguage from lib settings Revert embedded cover change Fixed bitrate fallback check Use overwriteFile setting when downloading embedded covers Fixed bitrate fallback not working Fixed some issues to make the lib work Implemented spotify plugin back Better handling of albums upcs Fixed queue item not cancelling correctly Code parity with deemix-js Code cleanup with pylint Even more rework on the library More work on the library (WIP) Total rework of the library (WIP) Some rework done on types Added start queue function Made nextitem work on a thread Removed dz as first parameter Started queuemanager refactoring Removed eventlet Co-authored-by: RemixDev <RemixDev64@gmail.com> Reviewed-on: https://git.freezer.life/RemixDev/deemix-py/pulls/4 Co-Authored-By: RemixDev <remixdev@noreply.localhost> Co-Committed-By: RemixDev <remixdev@noreply.localhost>
This commit is contained in:
@ -1,41 +1,42 @@
|
||||
import re
|
||||
import string
|
||||
from deezer import TrackFormats
|
||||
import os
|
||||
|
||||
USER_AGENT_HEADER = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) " \
|
||||
"Chrome/79.0.3945.130 Safari/537.36"
|
||||
|
||||
def canWrite(folder):
|
||||
return os.access(folder, os.W_OK)
|
||||
|
||||
def generateReplayGainString(trackGain):
|
||||
return "{0:.2f} dB".format((float(trackGain) + 18.4) * -1)
|
||||
|
||||
def getBitrateInt(txt):
|
||||
def getBitrateNumberFromText(txt):
|
||||
txt = str(txt).lower()
|
||||
if txt in ['flac', 'lossless', '9']:
|
||||
return TrackFormats.FLAC
|
||||
elif txt in ['mp3', '320', '3']:
|
||||
if txt in ['mp3', '320', '3']:
|
||||
return TrackFormats.MP3_320
|
||||
elif txt in ['128', '1']:
|
||||
if txt in ['128', '1']:
|
||||
return TrackFormats.MP3_128
|
||||
elif txt in ['360', '360_hq', '15']:
|
||||
if txt in ['360', '360_hq', '15']:
|
||||
return TrackFormats.MP4_RA3
|
||||
elif txt in ['360_mq', '14']:
|
||||
if txt in ['360_mq', '14']:
|
||||
return TrackFormats.MP4_RA2
|
||||
elif txt in ['360_lq', '13']:
|
||||
if txt in ['360_lq', '13']:
|
||||
return TrackFormats.MP4_RA1
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def changeCase(str, type):
|
||||
if type == "lower":
|
||||
return str.lower()
|
||||
elif type == "upper":
|
||||
return str.upper()
|
||||
elif type == "start":
|
||||
return string.capwords(str)
|
||||
elif type == "sentence":
|
||||
return str.capitalize()
|
||||
else:
|
||||
return str
|
||||
return None
|
||||
|
||||
def changeCase(txt, case_type):
|
||||
if case_type == "lower":
|
||||
return txt.lower()
|
||||
if case_type == "upper":
|
||||
return txt.upper()
|
||||
if case_type == "start":
|
||||
return string.capwords(txt)
|
||||
if case_type == "sentence":
|
||||
return txt.capitalize()
|
||||
return str
|
||||
|
||||
def removeFeatures(title):
|
||||
clean = title
|
||||
@ -48,7 +49,6 @@ def removeFeatures(title):
|
||||
clean = ' '.join(clean.split())
|
||||
return clean
|
||||
|
||||
|
||||
def andCommaConcat(lst):
|
||||
tot = len(lst)
|
||||
result = ""
|
||||
@ -61,62 +61,6 @@ def andCommaConcat(lst):
|
||||
result += ", "
|
||||
return result
|
||||
|
||||
|
||||
def getIDFromLink(link, type):
|
||||
if '?' in link:
|
||||
link = link[:link.find('?')]
|
||||
if link.endswith("/"):
|
||||
link = link[:-1]
|
||||
|
||||
if link.startswith("http") and 'open.spotify.com/' in link:
|
||||
if '&' in link: link = link[:link.find('&')]
|
||||
if type == "spotifyplaylist":
|
||||
return link[link.find("/playlist/") + 10:]
|
||||
if type == "spotifytrack":
|
||||
return link[link.find("/track/") + 7:]
|
||||
if type == "spotifyalbum":
|
||||
return link[link.find("/album/") + 7:]
|
||||
elif link.startswith("spotify:"):
|
||||
if type == "spotifyplaylist":
|
||||
return link[link.find("playlist:") + 9:]
|
||||
if type == "spotifytrack":
|
||||
return link[link.find("track:") + 6:]
|
||||
if type == "spotifyalbum":
|
||||
return link[link.find("album:") + 6:]
|
||||
elif type == "artisttop":
|
||||
return re.search(r"\/artist\/(\d+)\/top_track", link)[1]
|
||||
elif type == "artistdiscography":
|
||||
return re.search(r"\/artist\/(\d+)\/discography", link)[1]
|
||||
else:
|
||||
return link[link.rfind("/") + 1:]
|
||||
|
||||
|
||||
def getTypeFromLink(link):
|
||||
type = ''
|
||||
if 'spotify' in link:
|
||||
type = 'spotify'
|
||||
if 'playlist' in link:
|
||||
type += 'playlist'
|
||||
elif 'track' in link:
|
||||
type += 'track'
|
||||
elif 'album' in link:
|
||||
type += 'album'
|
||||
elif 'deezer' in link:
|
||||
if '/track' in link:
|
||||
type = 'track'
|
||||
elif '/playlist' in link:
|
||||
type = 'playlist'
|
||||
elif '/album' in link:
|
||||
type = 'album'
|
||||
elif re.search("\/artist\/(\d+)\/top_track", link):
|
||||
type = 'artisttop'
|
||||
elif re.search("\/artist\/(\d+)\/discography", link):
|
||||
type = 'artistdiscography'
|
||||
elif '/artist' in link:
|
||||
type = 'artist'
|
||||
return type
|
||||
|
||||
|
||||
def uniqueArray(arr):
|
||||
for iPrinc, namePrinc in enumerate(arr):
|
||||
for iRest, nRest in enumerate(arr):
|
||||
@ -129,11 +73,3 @@ def removeDuplicateArtists(artist, artists):
|
||||
for role in artist.keys():
|
||||
artist[role] = uniqueArray(artist[role])
|
||||
return (artist, artists)
|
||||
|
||||
def checkFolder(folder):
|
||||
try:
|
||||
os.makedirs(folder, exist_ok=True)
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
return False
|
||||
return os.access(folder, os.W_OK)
|
||||
|
26
deemix/utils/crypto.py
Normal file
26
deemix/utils/crypto.py
Normal file
@ -0,0 +1,26 @@
|
||||
import binascii
|
||||
|
||||
from Cryptodome.Cipher import Blowfish, AES
|
||||
from Cryptodome.Hash import MD5
|
||||
|
||||
def _md5(data):
|
||||
h = MD5.new()
|
||||
h.update(data.encode() if isinstance(data, str) else data)
|
||||
return h.hexdigest()
|
||||
|
||||
def _ecbCrypt(key, data):
|
||||
return binascii.hexlify(AES.new(key.encode(), AES.MODE_ECB).encrypt(data))
|
||||
|
||||
def _ecbDecrypt(key, data):
|
||||
return AES.new(key.encode(), AES.MODE_ECB).decrypt(binascii.unhexlify(data.encode("utf-8")))
|
||||
|
||||
def generateBlowfishKey(trackId):
|
||||
SECRET = 'g4el58wc0zvf9na1'
|
||||
idMd5 = _md5(trackId)
|
||||
bfKey = ""
|
||||
for i in range(16):
|
||||
bfKey += chr(ord(idMd5[i]) ^ ord(idMd5[i + 16]) ^ ord(SECRET[i]))
|
||||
return bfKey
|
||||
|
||||
def decryptChunk(key, data):
|
||||
return Blowfish.new(key, Blowfish.MODE_CBC, b"\x00\x01\x02\x03\x04\x05\x06\x07").decrypt(data)
|
@ -1,31 +0,0 @@
|
||||
import binascii
|
||||
from Cryptodome.Cipher import Blowfish, AES
|
||||
from Cryptodome.Hash import MD5
|
||||
|
||||
def _md5(data):
|
||||
h = MD5.new()
|
||||
h.update(str.encode(data) if isinstance(data, str) else data)
|
||||
return h.hexdigest()
|
||||
|
||||
def generateBlowfishKey(trackId):
|
||||
SECRET = 'g4el58wc' + '0zvf9na1'
|
||||
idMd5 = _md5(trackId)
|
||||
bfKey = ""
|
||||
for i in range(16):
|
||||
bfKey += chr(ord(idMd5[i]) ^ ord(idMd5[i + 16]) ^ ord(SECRET[i]))
|
||||
return bfKey
|
||||
|
||||
def generateStreamURL(sng_id, md5, media_version, format):
|
||||
urlPart = b'\xa4'.join(
|
||||
[str.encode(md5), str.encode(str(format)), str.encode(str(sng_id)), str.encode(str(media_version))])
|
||||
md5val = _md5(urlPart)
|
||||
step2 = str.encode(md5val) + b'\xa4' + urlPart + b'\xa4'
|
||||
step2 = step2 + (b'.' * (16 - (len(step2) % 16)))
|
||||
urlPart = binascii.hexlify(AES.new(b'jo6aey6haid2Teih', AES.MODE_ECB).encrypt(step2))
|
||||
return "https://e-cdns-proxy-" + md5[0] + ".dzcdn.net/mobile/1/" + urlPart.decode("utf-8")
|
||||
|
||||
def reverseStreamURL(url):
|
||||
urlPart = url[42:]
|
||||
step2 = AES.new(b'jo6aey6haid2Teih', AES.MODE_ECB).decrypt(binascii.unhexlify(urlPart.encode("utf-8")))
|
||||
(md5val, md5, format, sng_id, media_version, _) = step2.split(b'\xa4')
|
||||
return (sng_id.decode('utf-8'), md5.decode('utf-8'), media_version.decode('utf-8'), format.decode('utf-8'))
|
32
deemix/utils/deezer.py
Normal file
32
deemix/utils/deezer.py
Normal file
@ -0,0 +1,32 @@
|
||||
import requests
|
||||
from deemix.utils.crypto import _md5
|
||||
from deemix.utils import USER_AGENT_HEADER
|
||||
CLIENT_ID = "172365"
|
||||
CLIENT_SECRET = "fb0bec7ccc063dab0417eb7b0d847f34"
|
||||
|
||||
def getAccessToken(email, password):
|
||||
password = _md5(password)
|
||||
request_hash = _md5(''.join([CLIENT_ID, email, password, CLIENT_SECRET]))
|
||||
response = requests.get(
|
||||
'https://api.deezer.com/auth/token',
|
||||
params={
|
||||
'app_id': CLIENT_ID,
|
||||
'login': email,
|
||||
'password': password,
|
||||
'hash': request_hash
|
||||
},
|
||||
headers={"User-Agent": USER_AGENT_HEADER}
|
||||
).json()
|
||||
return response.get('access_token')
|
||||
|
||||
def getArtFromAccessToken(accessToken):
|
||||
session = requests.Session()
|
||||
session.get(
|
||||
"https://api.deezer.com/platform/generic/track/3135556",
|
||||
headers={"Authorization": f"Bearer {accessToken}", "User-Agent": USER_AGENT_HEADER}
|
||||
)
|
||||
response = session.get(
|
||||
'https://www.deezer.com/ajax/gw-light.php?method=user.getArl&input=3&api_version=1.0&api_token=null',
|
||||
headers={"User-Agent": USER_AGENT_HEADER}
|
||||
).json()
|
||||
return response.get('results')
|
@ -1,44 +1,72 @@
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
from deemix.utils import canWrite
|
||||
|
||||
homedata = Path.home()
|
||||
userdata = ""
|
||||
musicdata = ""
|
||||
|
||||
|
||||
if os.getenv("DEEMIX_DATA_DIR"):
|
||||
userdata = Path(os.getenv("DEEMIX_DATA_DIR"))
|
||||
elif os.getenv("XDG_CONFIG_HOME"):
|
||||
userdata = Path(os.getenv("XDG_CONFIG_HOME")) / 'deemix'
|
||||
elif os.getenv("APPDATA"):
|
||||
userdata = Path(os.getenv("APPDATA")) / "deemix"
|
||||
elif sys.platform.startswith('darwin'):
|
||||
userdata = homedata / 'Library' / 'Application Support' / 'deemix'
|
||||
else:
|
||||
userdata = homedata / '.config' / 'deemix'
|
||||
|
||||
if os.getenv("DEEMIX_MUSIC_DIR"):
|
||||
musicdata = Path(os.getenv("DEEMIX_MUSIC_DIR"))
|
||||
elif os.getenv("XDG_MUSIC_DIR"):
|
||||
musicdata = Path(os.getenv("XDG_MUSIC_DIR")) / "deemix Music"
|
||||
elif os.name == 'nt':
|
||||
import winreg
|
||||
sub_key = r'SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders'
|
||||
music_guid = '{4BD8D571-6D19-48D3-BE97-422220080E43}'
|
||||
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, sub_key) as key:
|
||||
location = None
|
||||
try: location = winreg.QueryValueEx(key, music_guid)[0]
|
||||
except: pass
|
||||
try: location = winreg.QueryValueEx(key, 'My Music')[0]
|
||||
except: pass
|
||||
if not location: location = homedata / "Music"
|
||||
musicdata = Path(location) / "deemix Music"
|
||||
else:
|
||||
musicdata = homedata / "Music" / "deemix Music"
|
||||
def checkPath(path):
|
||||
if path == "": return ""
|
||||
if not path.is_dir(): return ""
|
||||
if not canWrite(path): return ""
|
||||
return path
|
||||
|
||||
def getConfigFolder():
|
||||
global userdata
|
||||
if userdata != "": return userdata
|
||||
if os.getenv("XDG_CONFIG_HOME") and userdata == "":
|
||||
userdata = Path(os.getenv("XDG_CONFIG_HOME"))
|
||||
userdata = checkPath(userdata)
|
||||
if os.getenv("APPDATA") and userdata == "":
|
||||
userdata = Path(os.getenv("APPDATA"))
|
||||
userdata = checkPath(userdata)
|
||||
if sys.platform.startswith('darwin') and userdata == "":
|
||||
userdata = homedata / 'Library' / 'Application Support'
|
||||
userdata = checkPath(userdata)
|
||||
if userdata == "":
|
||||
userdata = homedata / '.config'
|
||||
userdata = checkPath(userdata)
|
||||
|
||||
if userdata == "": userdata = Path(os.getcwd()) / 'config'
|
||||
else: userdata = userdata / 'deemix'
|
||||
|
||||
if os.getenv("DEEMIX_DATA_DIR"):
|
||||
userdata = Path(os.getenv("DEEMIX_DATA_DIR"))
|
||||
return userdata
|
||||
|
||||
def getMusicFolder():
|
||||
global musicdata
|
||||
if musicdata != "": return musicdata
|
||||
if os.getenv("XDG_MUSIC_DIR") and musicdata == "":
|
||||
musicdata = Path(os.getenv("XDG_MUSIC_DIR"))
|
||||
musicdata = checkPath(musicdata)
|
||||
if (homedata / '.config' / 'user-dirs.dirs').is_file() and musicdata == "":
|
||||
with open(homedata / '.config' / 'user-dirs.dirs', 'r') as f:
|
||||
userDirs = f.read()
|
||||
musicdata = re.search(r"XDG_MUSIC_DIR=\"(.*)\"", userDirs).group(1)
|
||||
musicdata = Path(os.path.expandvars(musicdata))
|
||||
musicdata = checkPath(musicdata)
|
||||
if os.name == 'nt' and musicdata == "":
|
||||
musicKeys = ['My Music', '{4BD8D571-6D19-48D3-BE97-422220080E43}']
|
||||
regData = os.popen(r'reg.exe query "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"').read().split('\r\n')
|
||||
for i, line in enumerate(regData):
|
||||
if line == "": continue
|
||||
if i == 1: continue
|
||||
line = line.split(' ')
|
||||
if line[1] in musicKeys:
|
||||
musicdata = Path(line[3])
|
||||
break
|
||||
musicdata = checkPath(musicdata)
|
||||
if musicdata == "":
|
||||
musicdata = homedata / 'Music'
|
||||
musicdata = checkPath(musicdata)
|
||||
|
||||
if musicdata == "": musicdata = Path(os.getcwd()) / 'music'
|
||||
else: musicdata = musicdata / 'deemix Music'
|
||||
|
||||
if os.getenv("DEEMIX_MUSIC_DIR"):
|
||||
musicdata = Path(os.getenv("DEEMIX_MUSIC_DIR"))
|
||||
return musicdata
|
||||
|
@ -21,14 +21,13 @@ def fixName(txt, char='_'):
|
||||
txt = normalize("NFC", txt)
|
||||
return txt
|
||||
|
||||
def fixEndOfData(bString):
|
||||
try:
|
||||
bString.decode()
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
def fixLongName(name):
|
||||
def fixEndOfData(bString):
|
||||
try:
|
||||
bString.decode()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
if pathSep in name:
|
||||
sepName = name.split(pathSep)
|
||||
name = ""
|
||||
@ -52,30 +51,40 @@ def antiDot(string):
|
||||
return string
|
||||
|
||||
|
||||
def pad(num, max, settings):
|
||||
def pad(num, max_val, settings):
|
||||
if int(settings['paddingSize']) == 0:
|
||||
paddingSize = len(str(max))
|
||||
paddingSize = len(str(max_val))
|
||||
else:
|
||||
paddingSize = len(str(10 ** (int(settings['paddingSize']) - 1)))
|
||||
if paddingSize == 1:
|
||||
paddingSize = 2
|
||||
if settings['padTracks']:
|
||||
return str(num).zfill(paddingSize)
|
||||
return str(num)
|
||||
|
||||
def generatePath(track, downloadObject, settings):
|
||||
filenameTemplate = "%artist% - %title%"
|
||||
singleTrack = False
|
||||
if downloadObject.type == "track":
|
||||
if settings['createSingleFolder']:
|
||||
filenameTemplate = settings['albumTracknameTemplate']
|
||||
else:
|
||||
filenameTemplate = settings['tracknameTemplate']
|
||||
singleTrack = True
|
||||
elif downloadObject.type == "album":
|
||||
filenameTemplate = settings['albumTracknameTemplate']
|
||||
else:
|
||||
return str(num)
|
||||
filenameTemplate = settings['playlistTracknameTemplate']
|
||||
|
||||
def generateFilename(track, settings, template):
|
||||
filename = template or "%artist% - %title%"
|
||||
return settingsRegex(filename, track, settings)
|
||||
filename = generateTrackName(filenameTemplate, track, settings)
|
||||
|
||||
def generateFilepath(track, settings):
|
||||
filepath = Path(settings['downloadLocation'])
|
||||
filepath = Path(settings['downloadLocation'] or '.')
|
||||
artistPath = None
|
||||
coverPath = None
|
||||
extrasPath = None
|
||||
|
||||
if settings['createPlaylistFolder'] and track.playlist and not settings['tags']['savePlaylistAsCompilation']:
|
||||
filepath = filepath / settingsRegexPlaylist(settings['playlistNameTemplate'], track.playlist, settings)
|
||||
filepath = filepath / generatePlaylistName(settings['playlistNameTemplate'], track.playlist, settings)
|
||||
|
||||
if track.playlist and not settings['tags']['savePlaylistAsCompilation']:
|
||||
extrasPath = filepath
|
||||
@ -85,61 +94,66 @@ def generateFilepath(track, settings):
|
||||
(settings['createArtistFolder'] and track.playlist and settings['tags']['savePlaylistAsCompilation']) or
|
||||
(settings['createArtistFolder'] and track.playlist and settings['createStructurePlaylist'])
|
||||
):
|
||||
filepath = filepath / settingsRegexArtist(settings['artistNameTemplate'], track.album.mainArtist, settings, rootArtist=track.album.rootArtist)
|
||||
filepath = filepath / generateArtistName(settings['artistNameTemplate'], track.album.mainArtist, settings, rootArtist=track.album.rootArtist)
|
||||
artistPath = filepath
|
||||
|
||||
if (settings['createAlbumFolder'] and
|
||||
(not track.singleDownload or (track.singleDownload and settings['createSingleFolder'])) and
|
||||
(not singleTrack or (singleTrack and settings['createSingleFolder'])) and
|
||||
(not track.playlist or
|
||||
(track.playlist and settings['tags']['savePlaylistAsCompilation']) or
|
||||
(track.playlist and settings['createStructurePlaylist'])
|
||||
)
|
||||
):
|
||||
filepath = filepath / settingsRegexAlbum(settings['albumNameTemplate'], track.album, settings, track.playlist)
|
||||
filepath = filepath / generateAlbumName(settings['albumNameTemplate'], track.album, settings, track.playlist)
|
||||
coverPath = filepath
|
||||
|
||||
if not (track.playlist and not settings['tags']['savePlaylistAsCompilation']):
|
||||
extrasPath = filepath
|
||||
if not extrasPath: extrasPath = filepath
|
||||
|
||||
if (
|
||||
int(track.album.discTotal) > 1 and (
|
||||
int(track.album.discTotal) > 1 and (
|
||||
(settings['createAlbumFolder'] and settings['createCDFolder']) and
|
||||
(not track.singleDownload or (track.singleDownload and settings['createSingleFolder'])) and
|
||||
(not singleTrack or (singleTrack and settings['createSingleFolder'])) and
|
||||
(not track.playlist or
|
||||
(track.playlist and settings['tags']['savePlaylistAsCompilation']) or
|
||||
(track.playlist and settings['createStructurePlaylist'])
|
||||
)
|
||||
)
|
||||
)):
|
||||
filepath = filepath / f'CD{str(track.discNumber)}'
|
||||
filepath = filepath / f'CD{track.discNumber}'
|
||||
|
||||
return (filepath, artistPath, coverPath, extrasPath)
|
||||
# Remove subfolders from filename and add it to filepath
|
||||
if pathSep in filename:
|
||||
tempPath = filename[:filename.rfind(pathSep)]
|
||||
filepath = filepath / tempPath
|
||||
filename = filename[filename.rfind(pathSep) + len(pathSep):]
|
||||
|
||||
return (filename, filepath, artistPath, coverPath, extrasPath)
|
||||
|
||||
|
||||
def settingsRegex(filename, track, settings):
|
||||
filename = filename.replace("%title%", fixName(track.title, settings['illegalCharacterReplacer']))
|
||||
filename = filename.replace("%artist%", fixName(track.mainArtist.name, settings['illegalCharacterReplacer']))
|
||||
filename = filename.replace("%artists%", fixName(", ".join(track.artists), settings['illegalCharacterReplacer']))
|
||||
filename = filename.replace("%allartists%", fixName(track.artistsString, settings['illegalCharacterReplacer']))
|
||||
filename = filename.replace("%mainartists%", fixName(track.mainArtistsString, settings['illegalCharacterReplacer']))
|
||||
def generateTrackName(filename, track, settings):
|
||||
c = settings['illegalCharacterReplacer']
|
||||
filename = filename.replace("%title%", fixName(track.title, c))
|
||||
filename = filename.replace("%artist%", fixName(track.mainArtist.name, c))
|
||||
filename = filename.replace("%artists%", fixName(", ".join(track.artists), c))
|
||||
filename = filename.replace("%allartists%", fixName(track.artistsString, c))
|
||||
filename = filename.replace("%mainartists%", fixName(track.mainArtistsString, c))
|
||||
if track.featArtistsString:
|
||||
filename = filename.replace("%featartists%", fixName('('+track.featArtistsString+')', settings['illegalCharacterReplacer']))
|
||||
filename = filename.replace("%featartists%", fixName('('+track.featArtistsString+')', c))
|
||||
else:
|
||||
filename = filename.replace("%featartists%", '')
|
||||
filename = filename.replace("%album%", fixName(track.album.title, settings['illegalCharacterReplacer']))
|
||||
filename = filename.replace("%albumartist%", fixName(track.album.mainArtist.name, settings['illegalCharacterReplacer']))
|
||||
filename = filename.replace("%album%", fixName(track.album.title, c))
|
||||
filename = filename.replace("%albumartist%", fixName(track.album.mainArtist.name, c))
|
||||
filename = filename.replace("%tracknumber%", pad(track.trackNumber, track.album.trackTotal, settings))
|
||||
filename = filename.replace("%tracktotal%", str(track.album.trackTotal))
|
||||
filename = filename.replace("%discnumber%", str(track.discNumber))
|
||||
filename = filename.replace("%disctotal%", str(track.album.discTotal))
|
||||
if len(track.album.genre) > 0:
|
||||
filename = filename.replace("%genre%",
|
||||
fixName(track.album.genre[0], settings['illegalCharacterReplacer']))
|
||||
filename = filename.replace("%genre%", fixName(track.album.genre[0], c))
|
||||
else:
|
||||
filename = filename.replace("%genre%", "Unknown")
|
||||
filename = filename.replace("%year%", str(track.date.year))
|
||||
filename = filename.replace("%date%", track.dateString)
|
||||
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, c))
|
||||
filename = filename.replace("%isrc%", track.ISRC)
|
||||
filename = filename.replace("%upc%", track.album.barcode)
|
||||
filename = filename.replace("%explicit%", "(Explicit)" if track.explicit else "")
|
||||
@ -148,40 +162,41 @@ def settingsRegex(filename, track, settings):
|
||||
filename = filename.replace("%album_id%", str(track.album.id))
|
||||
filename = filename.replace("%artist_id%", str(track.mainArtist.id))
|
||||
if track.playlist:
|
||||
filename = filename.replace("%playlist_id%", str(track.playlist.playlistId))
|
||||
filename = filename.replace("%playlist_id%", str(track.playlist.playlistID))
|
||||
filename = filename.replace("%position%", pad(track.position, track.playlist.trackTotal, settings))
|
||||
else:
|
||||
filename = filename.replace("%playlist_id%", '')
|
||||
filename = filename.replace("%position%", pad(track.trackNumber, track.album.trackTotal, settings))
|
||||
filename = filename.replace("%position%", pad(track.position, track.album.trackTotal, settings))
|
||||
filename = filename.replace('\\', pathSep).replace('/', pathSep)
|
||||
return antiDot(fixLongName(filename))
|
||||
|
||||
|
||||
def settingsRegexAlbum(foldername, album, settings, playlist=None):
|
||||
def generateAlbumName(foldername, album, settings, playlist=None):
|
||||
c = settings['illegalCharacterReplacer']
|
||||
if playlist and settings['tags']['savePlaylistAsCompilation']:
|
||||
foldername = foldername.replace("%album_id%", "pl_" + str(playlist.playlistId))
|
||||
foldername = foldername.replace("%album_id%", "pl_" + str(playlist.playlistID))
|
||||
foldername = foldername.replace("%genre%", "Compile")
|
||||
else:
|
||||
foldername = foldername.replace("%album_id%", str(album.id))
|
||||
if len(album.genre) > 0:
|
||||
foldername = foldername.replace("%genre%", fixName(album.genre[0], settings['illegalCharacterReplacer']))
|
||||
foldername = foldername.replace("%genre%", fixName(album.genre[0], c))
|
||||
else:
|
||||
foldername = foldername.replace("%genre%", "Unknown")
|
||||
foldername = foldername.replace("%album%", fixName(album.title, settings['illegalCharacterReplacer']))
|
||||
foldername = foldername.replace("%artist%", fixName(album.mainArtist.name, settings['illegalCharacterReplacer']))
|
||||
foldername = foldername.replace("%album%", fixName(album.title, c))
|
||||
foldername = foldername.replace("%artist%", fixName(album.mainArtist.name, c))
|
||||
foldername = foldername.replace("%artist_id%", str(album.mainArtist.id))
|
||||
if album.rootArtist:
|
||||
foldername = foldername.replace("%root_artist%", fixName(album.rootArtist.name, settings['illegalCharacterReplacer']))
|
||||
foldername = foldername.replace("%root_artist%", fixName(album.rootArtist.name, c))
|
||||
foldername = foldername.replace("%root_artist_id%", str(album.rootArtist.id))
|
||||
else:
|
||||
foldername = foldername.replace("%root_artist%", fixName(album.mainArtist.name, settings['illegalCharacterReplacer']))
|
||||
foldername = foldername.replace("%root_artist%", fixName(album.mainArtist.name, c))
|
||||
foldername = foldername.replace("%root_artist_id%", str(album.mainArtist.id))
|
||||
foldername = foldername.replace("%tracktotal%", str(album.trackTotal))
|
||||
foldername = foldername.replace("%disctotal%", str(album.discTotal))
|
||||
foldername = foldername.replace("%type%", fixName(album.recordType.capitalize(), settings['illegalCharacterReplacer']))
|
||||
foldername = foldername.replace("%type%", fixName(album.recordType.capitalize(), c))
|
||||
foldername = foldername.replace("%upc%", album.barcode)
|
||||
foldername = foldername.replace("%explicit%", "(Explicit)" if album.explicit else "")
|
||||
foldername = foldername.replace("%label%", fixName(album.label, settings['illegalCharacterReplacer']))
|
||||
foldername = foldername.replace("%label%", fixName(album.label, c))
|
||||
foldername = foldername.replace("%year%", str(album.date.year))
|
||||
foldername = foldername.replace("%date%", album.dateString)
|
||||
foldername = foldername.replace("%bitrate%", bitrateLabels[int(album.bitrate)])
|
||||
@ -190,23 +205,25 @@ def settingsRegexAlbum(foldername, album, settings, playlist=None):
|
||||
return antiDot(fixLongName(foldername))
|
||||
|
||||
|
||||
def settingsRegexArtist(foldername, artist, settings, rootArtist=None):
|
||||
foldername = foldername.replace("%artist%", fixName(artist.name, settings['illegalCharacterReplacer']))
|
||||
def generateArtistName(foldername, artist, settings, rootArtist=None):
|
||||
c = settings['illegalCharacterReplacer']
|
||||
foldername = foldername.replace("%artist%", fixName(artist.name, c))
|
||||
foldername = foldername.replace("%artist_id%", str(artist.id))
|
||||
if rootArtist:
|
||||
foldername = foldername.replace("%root_artist%", fixName(rootArtist.name, settings['illegalCharacterReplacer']))
|
||||
foldername = foldername.replace("%root_artist%", fixName(rootArtist.name, c))
|
||||
foldername = foldername.replace("%root_artist_id%", str(rootArtist.id))
|
||||
else:
|
||||
foldername = foldername.replace("%root_artist%", fixName(artist.name, settings['illegalCharacterReplacer']))
|
||||
foldername = foldername.replace("%root_artist%", fixName(artist.name, c))
|
||||
foldername = foldername.replace("%root_artist_id%", str(artist.id))
|
||||
foldername = foldername.replace('\\', pathSep).replace('/', pathSep)
|
||||
return antiDot(fixLongName(foldername))
|
||||
|
||||
|
||||
def settingsRegexPlaylist(foldername, playlist, settings):
|
||||
foldername = foldername.replace("%playlist%", fixName(playlist.title, settings['illegalCharacterReplacer']))
|
||||
foldername = foldername.replace("%playlist_id%", fixName(playlist.playlistId, settings['illegalCharacterReplacer']))
|
||||
foldername = foldername.replace("%owner%", fixName(playlist.owner['name'], settings['illegalCharacterReplacer']))
|
||||
def generatePlaylistName(foldername, playlist, settings):
|
||||
c = settings['illegalCharacterReplacer']
|
||||
foldername = foldername.replace("%playlist%", fixName(playlist.title, c))
|
||||
foldername = foldername.replace("%playlist_id%", fixName(playlist.playlistID, c))
|
||||
foldername = foldername.replace("%owner%", fixName(playlist.owner['name'], c))
|
||||
foldername = foldername.replace("%owner_id%", str(playlist.owner['id']))
|
||||
foldername = foldername.replace("%year%", str(playlist.date.year))
|
||||
foldername = foldername.replace("%date%", str(playlist.dateString))
|
||||
@ -214,12 +231,13 @@ def settingsRegexPlaylist(foldername, playlist, settings):
|
||||
foldername = foldername.replace('\\', pathSep).replace('/', pathSep)
|
||||
return antiDot(fixLongName(foldername))
|
||||
|
||||
def settingsRegexPlaylistFile(foldername, queueItem, settings):
|
||||
foldername = foldername.replace("%title%", fixName(queueItem.title, settings['illegalCharacterReplacer']))
|
||||
foldername = foldername.replace("%artist%", fixName(queueItem.artist, settings['illegalCharacterReplacer']))
|
||||
def generateDownloadObjectName(foldername, queueItem, settings):
|
||||
c = settings['illegalCharacterReplacer']
|
||||
foldername = foldername.replace("%title%", fixName(queueItem.title, c))
|
||||
foldername = foldername.replace("%artist%", fixName(queueItem.artist, c))
|
||||
foldername = foldername.replace("%size%", str(queueItem.size))
|
||||
foldername = foldername.replace("%type%", fixName(queueItem.type, settings['illegalCharacterReplacer']))
|
||||
foldername = foldername.replace("%id%", fixName(queueItem.id, settings['illegalCharacterReplacer']))
|
||||
foldername = foldername.replace("%type%", fixName(queueItem.type, c))
|
||||
foldername = foldername.replace("%id%", fixName(queueItem.id, c))
|
||||
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, c)
|
||||
return antiDot(fixLongName(foldername))
|
||||
|
@ -1,212 +0,0 @@
|
||||
from mutagen.flac import FLAC, Picture
|
||||
from mutagen.id3 import ID3, ID3NoHeaderError, \
|
||||
TXXX, TIT2, TPE1, TALB, TPE2, TRCK, TPOS, TCON, TYER, TDAT, TLEN, TBPM, \
|
||||
TPUB, TSRC, USLT, SYLT, APIC, IPLS, TCOM, TCOP, TCMP, Encoding, PictureType
|
||||
|
||||
# Adds tags to a MP3 file
|
||||
def tagID3(stream, track, save):
|
||||
# Delete exsisting tags
|
||||
try:
|
||||
tag = ID3(stream)
|
||||
tag.delete()
|
||||
except ID3NoHeaderError:
|
||||
tag = ID3()
|
||||
|
||||
if save['title']:
|
||||
tag.add(TIT2(text=track.title))
|
||||
|
||||
if save['artist'] and len(track.artists):
|
||||
if save['multiArtistSeparator'] == "default":
|
||||
tag.add(TPE1(text=track.artists))
|
||||
else:
|
||||
if save['multiArtistSeparator'] == "nothing":
|
||||
tag.add(TPE1(text=track.mainArtist.name))
|
||||
else:
|
||||
tag.add(TPE1(text=track.artistsString))
|
||||
# Tag ARTISTS is added to keep the multiartist support when using a non standard tagging method
|
||||
# https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html#artists
|
||||
tag.add(TXXX(desc="ARTISTS", text=track.artists))
|
||||
|
||||
if save['album']:
|
||||
tag.add(TALB(text=track.album.title))
|
||||
|
||||
if save['albumArtist'] and len(track.album.artists):
|
||||
if save['singleAlbumArtist'] and track.album.mainArtist.save:
|
||||
tag.add(TPE2(text=track.album.mainArtist.name))
|
||||
else:
|
||||
tag.add(TPE2(text=track.album.artists))
|
||||
|
||||
if save['trackNumber']:
|
||||
trackNumber = str(track.trackNumber)
|
||||
if save['trackTotal']:
|
||||
trackNumber += "/" + str(track.album.trackTotal)
|
||||
tag.add(TRCK(text=trackNumber))
|
||||
if save['discNumber']:
|
||||
discNumber = str(track.discNumber)
|
||||
if save['discTotal']:
|
||||
discNumber += "/" + str(track.album.discTotal)
|
||||
tag.add(TPOS(text=discNumber))
|
||||
|
||||
if save['genre']:
|
||||
tag.add(TCON(text=track.album.genre))
|
||||
if save['year']:
|
||||
tag.add(TYER(text=str(track.date.year)))
|
||||
if save['date']:
|
||||
# Referencing ID3 standard
|
||||
# https://id3.org/id3v2.3.0#TDAT
|
||||
# The 'Date' frame is a numeric string in the DDMM format.
|
||||
tag.add(TDAT(text=str(track.date.day) + str(track.date.month)))
|
||||
if save['length']:
|
||||
tag.add(TLEN(text=str(int(track.duration)*1000)))
|
||||
if save['bpm']:
|
||||
tag.add(TBPM(text=str(track.bpm)))
|
||||
if save['label']:
|
||||
tag.add(TPUB(text=track.album.label))
|
||||
if save['isrc']:
|
||||
tag.add(TSRC(text=track.ISRC))
|
||||
if save['barcode']:
|
||||
tag.add(TXXX(desc="BARCODE", text=track.album.barcode))
|
||||
if save['explicit']:
|
||||
tag.add(TXXX(desc="ITUNESADVISORY", text= "1" if track.explicit else "0" ))
|
||||
if save['replayGain']:
|
||||
tag.add(TXXX(desc="REPLAYGAIN_TRACK_GAIN", text=track.replayGain))
|
||||
if track.lyrics.unsync and save['lyrics']:
|
||||
tag.add(USLT(text=track.lyrics.unsync))
|
||||
if track.lyrics.syncID3 and save['syncedLyrics']:
|
||||
# Referencing ID3 standard
|
||||
# https://id3.org/id3v2.3.0#sec4.10
|
||||
# Type: 1 => is lyrics
|
||||
# Format: 2 => Absolute time, 32 bit sized, using milliseconds as unit
|
||||
tag.add(SYLT(Encoding.UTF8, type=1, format=2, text=track.lyrics.syncID3))
|
||||
|
||||
involved_people = []
|
||||
for role in track.contributors:
|
||||
if role in ['author', 'engineer', 'mixer', 'producer', 'writer']:
|
||||
for person in track.contributors[role]:
|
||||
involved_people.append([role, person])
|
||||
elif role == 'composer' and save['composer']:
|
||||
tag.add(TCOM(text=track.contributors['composer']))
|
||||
if len(involved_people) > 0 and save['involvedPeople']:
|
||||
tag.add(IPLS(people=involved_people))
|
||||
|
||||
if save['copyright']:
|
||||
tag.add(TCOP(text=track.copyright))
|
||||
if save['savePlaylistAsCompilation'] and track.playlist or track.album.recordType == "compile":
|
||||
tag.add(TCMP(text="1"))
|
||||
|
||||
if save['source']:
|
||||
tag.add(TXXX(desc="SOURCE", text='Deezer'))
|
||||
tag.add(TXXX(desc="SOURCEID", text=str(track.id)))
|
||||
|
||||
if save['cover'] and track.album.embeddedCoverPath:
|
||||
|
||||
descEncoding = Encoding.LATIN1
|
||||
if save['coverDescriptionUTF8']:
|
||||
descEncoding = Encoding.UTF8
|
||||
|
||||
mimeType = 'image/jpeg'
|
||||
if str(track.album.embeddedCoverPath).endswith('png'):
|
||||
mimeType = 'image/png'
|
||||
|
||||
with open(track.album.embeddedCoverPath, 'rb') as f:
|
||||
tag.add(APIC(descEncoding, mimeType, PictureType.COVER_FRONT, desc='cover', data=f.read()))
|
||||
|
||||
tag.save( stream,
|
||||
v1=2 if save['saveID3v1'] else 0,
|
||||
v2_version=3,
|
||||
v23_sep=None if save['useNullSeparator'] else '/' )
|
||||
|
||||
# Adds tags to a FLAC file
|
||||
def tagFLAC(stream, track, save):
|
||||
# Delete exsisting tags
|
||||
tag = FLAC(stream)
|
||||
tag.delete()
|
||||
tag.clear_pictures()
|
||||
|
||||
if save['title']:
|
||||
tag["TITLE"] = track.title
|
||||
|
||||
if save['artist'] and len(track.artists):
|
||||
if save['multiArtistSeparator'] == "default":
|
||||
tag["ARTIST"] = track.artists
|
||||
else:
|
||||
if save['multiArtistSeparator'] == "nothing":
|
||||
tag["ARTIST"] = track.mainArtist.name
|
||||
else:
|
||||
tag["ARTIST"] = track.artistsString
|
||||
# Tag ARTISTS is added to keep the multiartist support when using a non standard tagging method
|
||||
# https://picard-docs.musicbrainz.org/en/technical/tag_mapping.html#artists
|
||||
tag["ARTISTS"] = track.artists
|
||||
|
||||
if save['album']:
|
||||
tag["ALBUM"] = track.album.title
|
||||
|
||||
if save['albumArtist'] and len(track.album.artists):
|
||||
if save['singleAlbumArtist'] and track.album.mainArtist.save:
|
||||
tag["ALBUMARTIST"] = track.album.mainArtist.name
|
||||
else:
|
||||
tag["ALBUMARTIST"] = track.album.artists
|
||||
|
||||
if save['trackNumber']:
|
||||
tag["TRACKNUMBER"] = str(track.trackNumber)
|
||||
if save['trackTotal']:
|
||||
tag["TRACKTOTAL"] = str(track.album.trackTotal)
|
||||
if save['discNumber']:
|
||||
tag["DISCNUMBER"] = str(track.discNumber)
|
||||
if save['discTotal']:
|
||||
tag["DISCTOTAL"] = str(track.album.discTotal)
|
||||
if save['genre']:
|
||||
tag["GENRE"] = track.album.genre
|
||||
|
||||
# YEAR tag is not suggested as a standard tag
|
||||
# Being YEAR already contained in DATE will only use DATE instead
|
||||
# Reference: https://www.xiph.org/vorbis/doc/v-comment.html#fieldnames
|
||||
if save['date']:
|
||||
tag["DATE"] = track.dateString
|
||||
elif save['year']:
|
||||
tag["DATE"] = str(track.date.year)
|
||||
|
||||
if save['length']:
|
||||
tag["LENGTH"] = str(int(track.duration)*1000)
|
||||
if save['bpm']:
|
||||
tag["BPM"] = str(track.bpm)
|
||||
if save['label']:
|
||||
tag["PUBLISHER"] = track.album.label
|
||||
if save['isrc']:
|
||||
tag["ISRC"] = track.ISRC
|
||||
if save['barcode']:
|
||||
tag["BARCODE"] = track.album.barcode
|
||||
if save['explicit']:
|
||||
tag["ITUNESADVISORY"] = "1" if track.explicit else "0"
|
||||
if save['replayGain']:
|
||||
tag["REPLAYGAIN_TRACK_GAIN"] = track.replayGain
|
||||
if track.lyrics.unsync and save['lyrics']:
|
||||
tag["LYRICS"] = track.lyrics.unsync
|
||||
|
||||
for role in track.contributors:
|
||||
if role in ['author', 'engineer', 'mixer', 'producer', 'writer', 'composer']:
|
||||
if save['involvedPeople'] and role != 'composer' or save['composer'] and role == 'composer':
|
||||
tag[role] = track.contributors[role]
|
||||
elif role == 'musicpublisher' and save['involvedPeople']:
|
||||
tag["ORGANIZATION"] = track.contributors['musicpublisher']
|
||||
|
||||
if save['copyright']:
|
||||
tag["COPYRIGHT"] = track.copyright
|
||||
if save['savePlaylistAsCompilation'] and track.playlist or track.album.recordType == "compile":
|
||||
tag["COMPILATION"] = "1"
|
||||
|
||||
if save['source']:
|
||||
tag["SOURCE"] = 'Deezer'
|
||||
tag["SOURCEID"] = str(track.id)
|
||||
|
||||
if save['cover'] and track.album.embeddedCoverPath:
|
||||
image = Picture()
|
||||
image.type = PictureType.COVER_FRONT
|
||||
image.mime = 'image/jpeg'
|
||||
if str(track.album.embeddedCoverPath).endswith('png'):
|
||||
image.mime = 'image/png'
|
||||
with open(track.album.embeddedCoverPath, 'rb') as f:
|
||||
image.data = f.read()
|
||||
tag.add_picture(image)
|
||||
|
||||
tag.save(deleteid3=True)
|
Reference in New Issue
Block a user