Files
orpheusdl-deezer/interface.py
nanometer5088 ac0da8f22c Moved Deezer keys to external sources
Signed-off-by: nanometer5088 <code.deleo@simplelogin.com>
2023-08-03 23:42:53 -03:00

403 lines
17 KiB
Python
Executable File

import re, sys
from enum import Enum, auto
from urllib.parse import urlparse
from requests import get
from utils.models import *
from utils.utils import create_temp_filename
from .dzapi import DeezerAPI
module_information = ModuleInformation(
service_name = 'Deezer',
module_supported_modes = ModuleModes.download | ModuleModes.lyrics | ModuleModes.covers | ModuleModes.credits,
global_settings = {'client_id': '447462',
'client_secret': 'a83bf7f38ad2f137e444727cfc3775cf',
'bf_secret': '',
'track_url_key': '',
'prefer_mhm1': False},
session_settings = {'arl': '',
'username': 'DO NOT MODIFY',
'password': 'DO NOT MODIFY'},
session_storage_variables = ['arl'],
netlocation_constant = 'deezer',
url_decoding = ManualEnum.manual,
test_url = 'https://www.deezer.com/track/3135556',
)
class ImageType(Enum):
cover = auto(),
artist = auto(),
playlist = auto(),
user = auto(),
misc = auto(),
talk = auto()
class ModuleInterface:
def __init__(self, module_controller: ModuleController):
self.settings = module_controller.module_settings
self.exception = module_controller.module_error
self.tsc = module_controller.temporary_settings_controller
self.default_cover = module_controller.orpheus_options.default_cover_options
self.disable_subscription_check = module_controller.orpheus_options.disable_subscription_check
if self.default_cover.file_type is ImageFileTypeEnum.webp:
self.default_cover.file_type = ImageFileTypeEnum.jpg
self.session = DeezerAPI(self.exception, self.settings['client_id'], self.settings['client_secret'], self.settings['bf_secret'], self.settings['track_url_key'])
arl = self.settings['arl']
if arl:
try:
self.session.login_via_arl(arl)
except:
print("ARL Invalid")
sys.exit(1)
self.quality_parse = {
QualityEnum.MINIMUM: 'MP3_128',
QualityEnum.LOW: 'MP3_128',
QualityEnum.MEDIUM: 'MP3_320',
QualityEnum.HIGH: 'MP3_320',
QualityEnum.LOSSLESS: 'FLAC',
QualityEnum.HIFI: 'FLAC'
}
self.format = self.quality_parse[module_controller.orpheus_options.quality_tier]
self.compression_nums = {
CoverCompressionEnum.high: 80,
CoverCompressionEnum.low: 50
}
if arl:
self.check_sub()
def login(self, email: str, password: str):
try:
self.check_sub()
except:
print("Missing ARL, add in on config file")
sys.exit(1)
def custom_url_parse(self, link):
url = urlparse(link)
if url.hostname == 'deezer.page.link':
r = get('https://deezer.page.link' + url.path, allow_redirects=False)
if r.status_code != 302:
raise self.exception(f'Invalid URL: {link}')
url = urlparse(r.headers['Location'])
path_match = re.match(r'^\/(?:[a-z]{2}\/)?(track|album|artist|playlist)\/(\d+)\/?$', url.path)
if not path_match:
raise self.exception(f'Invalid URL: {link}')
return MediaIdentification(
media_type = DownloadTypeEnum[path_match.group(1)],
media_id = path_match.group(2)
)
def get_track_info(self, track_id: str, quality_tier: QualityEnum, codec_options: CodecOptions, data={}, alb_tags={}) -> TrackInfo:
is_user_upped = int(track_id) < 0
format = self.quality_parse[quality_tier] if not is_user_upped else 'MP3_MISC'
track = None
if data and track_id in data:
track = data[track_id]
elif not is_user_upped:
track = self.session.get_track(track_id)
else: # user-upped tracks can't be requested with deezer.pageTrack
track = self.session.get_track_data(track_id)
t_data = track
if not is_user_upped:
t_data = t_data['DATA']
if 'FALLBACK' in t_data:
t_data = t_data['FALLBACK']
tags = Tags(
track_number = t_data.get('TRACK_NUMBER'),
copyright = t_data.get('COPYRIGHT'),
isrc = t_data['ISRC'],
disc_number = t_data.get('DISK_NUMBER'),
replay_gain = t_data.get('GAIN'),
release_date = t_data.get('PHYSICAL_RELEASE_DATE'),
)
for key in alb_tags:
setattr(tags, key, alb_tags[key])
error = None
if not is_user_upped:
premium_formats = ['FLAC', 'MP3_320']
countries = t_data['AVAILABLE_COUNTRIES']['STREAM_ADS']
if not countries:
error = 'Track not available'
elif format in premium_formats:
formats_360 = ['MP4_RA3', 'MP4_RA2', 'MP4_RA1'] if not self.settings['prefer_mhm1'] else ['MHM1_RA3', 'MHM1_RA2', 'MHM1_RA1']
if quality_tier is QualityEnum.HIFI and codec_options.spatial_codecs:
# deezer has three different 360ra qualities, so this checks the highest quality one available
# if there isn't any it just gets FLAC instead
for f in formats_360:
if self.session.check_format(t_data['MD5_ORIGIN'], f, t_data['SNG_ID'], t_data['MEDIA_VERSION']):
format = f
break
if format not in formats_360:
formats_to_check = premium_formats
while len(formats_to_check) != 0:
if formats_to_check[0] != format:
formats_to_check.pop(0)
else:
break
temp_f = None
for f in formats_to_check:
if t_data[f'FILESIZE_{f}'] != '0':
temp_f = f
break
if temp_f is None:
temp_f = 'MP3_128'
format = temp_f
if self.session.country not in countries:
error = 'Track not available in your country, try downloading in 128/360RA instead'
elif format not in self.session.available_formats:
error = 'Format not available by your subscription'
codec = {
'MP3_MISC': CodecEnum.MP3,
'MP3_128': CodecEnum.MP3,
'MP3_320': CodecEnum.MP3,
'FLAC': CodecEnum.FLAC,
'MP4_RA1': CodecEnum.MHA1,
'MP4_RA2': CodecEnum.MHA1,
'MP4_RA3': CodecEnum.MHA1,
'MHM1_RA1': CodecEnum.MHM1,
'MHM1_RA2': CodecEnum.MHM1,
'MHM1_RA3': CodecEnum.MHM1,
}[format]
bitrate = {
'MP3_MISC': None,
'MP3_128': 128,
'MP3_320': 320,
'FLAC': 1411,
'MP4_RA1': None,
'MP4_RA2': None,
'MP4_RA3': None,
'MHM1_RA1': None,
'MHM1_RA2': None,
'MHM1_RA3': None,
}[format]
download_extra_kwargs = {
'id': t_data['SNG_ID'],
'track_token': t_data['TRACK_TOKEN'],
'track_token_expiry': t_data['TRACK_TOKEN_EXPIRE'],
'format': format,
'md5_origin': t_data['MD5_ORIGIN'],
'media_version': t_data['MEDIA_VERSION']
}
return TrackInfo(
name = t_data['SNG_TITLE'] if not t_data.get('VERSION') else f'{t_data["SNG_TITLE"]} {t_data["VERSION"]}',
album_id = t_data['ALB_ID'],
album = t_data['ALB_TITLE'],
artists = [a['ART_NAME'] for a in t_data['ARTISTS']] if 'ARTISTS' in t_data else [t_data['ART_NAME']],
tags = tags,
codec = codec,
cover_url = self.get_image_url(t_data['ALB_PICTURE'], ImageType.cover, ImageFileTypeEnum.jpg, self.default_cover.resolution, self.compression_nums[self.default_cover.compression]),
release_year = tags.release_date.split('-')[0] if tags.release_date else None,
explicit = t_data['EXPLICIT_LYRICS'] == '1' if 'EXPLICIT_LYRICS' in t_data else None,
artist_id = t_data['ART_ID'],
bit_depth = 24 if codec in (CodecEnum.MHA1, CodecEnum.MHM1) else 16,
sample_rate = 48 if codec in (CodecEnum.MHA1, CodecEnum.MHM1) else 44.1,
bitrate = bitrate,
download_extra_kwargs = download_extra_kwargs,
cover_extra_kwargs = {'data': {track_id: t_data['ALB_PICTURE']}},
credits_extra_kwargs = {'data': {track_id: t_data.get('SNG_CONTRIBUTORS')}},
lyrics_extra_kwargs = {'data': {track_id: track.get('LYRICS')}},
error = error
)
def get_track_download(self, id, track_token, track_token_expiry, format, md5_origin, media_version):
path = create_temp_filename()
# legacy urls don't have country restrictions, but aren't available for 320 and flac
# you can still get shit like 360RA with those though. bruh moment
if format in ('MP3_320', 'FLAC'):
url = self.session.get_track_url(id, track_token, track_token_expiry, format)
else:
url = self.session.get_legacy_track_url(md5_origin, format, id, media_version)
self.session.dl_track(id, url, path)
return TrackDownloadInfo(
download_type = DownloadEnum.TEMP_FILE_PATH,
temp_file_path = path
)
def get_album_info(self, album_id: str, data={}) -> Optional[AlbumInfo]:
album = data[album_id] if album_id in data else self.session.get_album(album_id)
a_data = album['DATA']
# placeholder images can't be requested as pngs
cover_type = self.default_cover.file_type if a_data['ALB_PICTURE'] != '' else ImageFileTypeEnum.jpg
tracks_data = album['SONGS']['data']
try:
total_tracks = int(tracks_data[-1]['TRACK_NUMBER'])
total_discs = int(tracks_data[-1]['DISK_NUMBER'])
except IndexError:
total_tracks = 0
total_discs = 0
alb_tags = {
'total_tracks': total_tracks,
'total_discs': total_discs,
'upc': a_data['UPC'],
'label': a_data['LABEL_NAME'],
'album_artist': a_data['ART_NAME'],
'release_date': a_data.get('ORIGINAL_RELEASE_DATE') or a_data['PHYSICAL_RELEASE_DATE']
}
return AlbumInfo(
name = a_data['ALB_TITLE'],
artist = a_data['ART_NAME'],
tracks = [track['SNG_ID'] for track in tracks_data],
release_year = alb_tags['release_date'].split('-')[0],
explicit = a_data['EXPLICIT_ALBUM_CONTENT']['EXPLICIT_LYRICS_STATUS'] in (1, 4),
artist_id = a_data['ART_ID'],
cover_url = self.get_image_url(a_data['ALB_PICTURE'], ImageType.cover, cover_type, self.default_cover.resolution, self.compression_nums[self.default_cover.compression]),
cover_type = cover_type,
all_track_cover_jpg_url = self.get_image_url(a_data['ALB_PICTURE'], ImageType.cover, ImageFileTypeEnum.jpg, self.default_cover.resolution, self.compression_nums[self.default_cover.compression]),
track_extra_kwargs = {'alb_tags': alb_tags},
)
def get_playlist_info(self, playlist_id: str, data={}) -> PlaylistInfo:
playlist = data[playlist_id] if playlist_id in data else self.session.get_playlist(playlist_id, -1, 0)
p_data = playlist['DATA']
# placeholder images can't be requested as pngs
cover_type = self.default_cover.file_type if p_data['PLAYLIST_PICTURE'] != '' else ImageFileTypeEnum.jpg
user_upped_dict = {}
for t in playlist['SONGS']['data']:
if int(t['SNG_ID']) < 0:
user_upped_dict[t['SNG_ID']] = t
return PlaylistInfo(
name = p_data['TITLE'],
creator = p_data['PARENT_USERNAME'],
tracks = [t['SNG_ID'] for t in playlist['SONGS']['data']],
release_year = p_data['DATE_ADD'].split('-')[0],
creator_id = p_data['PARENT_USER_ID'],
cover_url = self.get_image_url(p_data['PLAYLIST_PICTURE'], ImageType.playlist, cover_type, self.default_cover.resolution, self.compression_nums[self.default_cover.compression]),
cover_type = cover_type,
description = p_data['DESCRIPTION'],
track_extra_kwargs = {'data': user_upped_dict}
)
def get_artist_info(self, artist_id: str, get_credited_albums: bool, artist_name = None) -> ArtistInfo:
name = artist_name if artist_name else self.session.get_artist_name(artist_id)
return ArtistInfo(
name = name,
albums = self.session.get_artist_album_ids(artist_id, 0, -1, get_credited_albums),
)
def get_track_credits(self, track_id: str, data={}):
if int(track_id) < 0:
return []
credits = data[track_id] if track_id in data else self.session.get_track_contributors(track_id)
if not credits:
return []
# fixes tagging conflict with normal artist tag, it's redundant anyways
credits.pop('artist', None)
return [CreditsInfo(k, v) for k, v in credits.items()]
def get_track_cover(self, track_id: str, cover_options: CoverOptions, data={}) -> CoverInfo:
cover_md5 = data[track_id] if track_id in data else self.session.get_track_cover(track_id)
# placeholder images can't be requested as pngs
file_type = cover_options.file_type if cover_md5 != '' and cover_options.file_type is not ImageFileTypeEnum.webp else ImageFileTypeEnum.jpg
url = self.get_image_url(cover_md5, ImageType.cover, file_type, cover_options.resolution, self.compression_nums[cover_options.compression])
return CoverInfo(url=url, file_type=file_type)
def get_track_lyrics(self, track_id: str, data={}) -> LyricsInfo:
if int(track_id) < 0:
return LyricsInfo()
try:
lyrics = data[track_id] if track_id in data else self.session.get_track_lyrics(track_id)
except self.exception:
return LyricsInfo()
if not lyrics:
return LyricsInfo()
synced_text = None
if 'LYRICS_SYNC_JSON' in lyrics:
synced_text = ''
for line in lyrics['LYRICS_SYNC_JSON']:
if 'lrc_timestamp' in line:
synced_text += f'{line["lrc_timestamp"]}{line["line"]}\n'
else:
synced_text += '\n'
return LyricsInfo(embedded=lyrics['LYRICS_TEXT'], synced=synced_text)
def search(self, query_type: DownloadTypeEnum, query: str, track_info: TrackInfo = None, limit: int = 10):
results = {}
if track_info and track_info.tags.isrc:
results = [self.session.get_track_data_by_isrc(track_info.tags.isrc)]
if not results:
results = self.session.search(query, query_type.name, 0, limit)['data']
if query_type is DownloadTypeEnum.track:
return [SearchResult(
result_id = i['SNG_ID'],
name = i['SNG_TITLE'] if not i.get('VERSION') else f'{i["SNG_TITLE"]} {i["VERSION"]}',
artists = [a['ART_NAME'] for a in i['ARTISTS']],
explicit = i['EXPLICIT_LYRICS'] == '1',
additional = [i["ALB_TITLE"]]
) for i in results]
elif query_type is DownloadTypeEnum.album:
return [SearchResult(
result_id = i['ALB_ID'],
name = i['ALB_TITLE'],
artists = [a['ART_NAME'] for a in i['ARTISTS']],
year = i['PHYSICAL_RELEASE_DATE'].split('-')[0],
explicit = i['EXPLICIT_ALBUM_CONTENT']['EXPLICIT_LYRICS_STATUS'] in (1, 4),
additional = [i["NUMBER_TRACK"]]
) for i in results]
elif query_type is DownloadTypeEnum.artist:
return [SearchResult(
result_id = i['ART_ID'],
name = i['ART_NAME'],
extra_kwargs = {'artist_name': i['ART_NAME']}
) for i in results]
elif query_type is DownloadTypeEnum.playlist:
return [SearchResult(
result_id = i['PLAYLIST_ID'],
name = i['TITLE'],
artists = [i['PARENT_USERNAME']],
additional = [i["NB_SONG"]]
) for i in results]
def get_image_url(self, md5, img_type: ImageType, file_type: ImageFileTypeEnum, res, compression):
if res > 3000:
res = 3000
filename = {
ImageFileTypeEnum.jpg: f'{res}x0-000000-{compression}-0-0.jpg',
ImageFileTypeEnum.png: f'{res}x0-none-100-0-0.png'
}[file_type]
return f'https://cdns-images.dzcdn.net/images/{img_type.name}/{md5}/{filename}'
def check_sub(self):
if not self.disable_subscription_check and (self.format not in self.session.available_formats):
print('Deezer: quality set in the settings is not accessible by the current subscription')