Merge branch 'develop'
This commit is contained in:
224
interface.py
224
interface.py
@@ -23,7 +23,7 @@ module_information = ModuleInformation(
|
|||||||
global_settings={
|
global_settings={
|
||||||
'tv_token': '7m7Ap0JC9j1cOM3n',
|
'tv_token': '7m7Ap0JC9j1cOM3n',
|
||||||
'tv_secret': 'vRAdA108tlvkJpTsGZS8rGZ7xTlbJ0qaZ2K9saEzsgY=',
|
'tv_secret': 'vRAdA108tlvkJpTsGZS8rGZ7xTlbJ0qaZ2K9saEzsgY=',
|
||||||
'mobile_atmos_token': 'dN2N95wCyEBTllu4',
|
'mobile_atmos_token': 'km8T1xS355y7dd3H',
|
||||||
'mobile_default_token': 'WAU9gXp3tHhK4Nns',
|
'mobile_default_token': 'WAU9gXp3tHhK4Nns',
|
||||||
'enable_mobile': True,
|
'enable_mobile': True,
|
||||||
'force_non_spatial': False,
|
'force_non_spatial': False,
|
||||||
@@ -74,86 +74,81 @@ class ModuleInterface:
|
|||||||
if not saved_sessions:
|
if not saved_sessions:
|
||||||
saved_sessions = {}
|
saved_sessions = {}
|
||||||
|
|
||||||
if self.settings['enable_mobile']:
|
if not self.settings['enable_mobile']:
|
||||||
# check all saved session for a session starting with "MOBILE"
|
|
||||||
if not any(session for session in saved_sessions.keys() if session[:6] == 'MOBILE'):
|
|
||||||
confirm = input(' "enable_mobile" is enabled but no MOBILE session was found. Do you want to create a '
|
|
||||||
'MOBILE session (used for AC-4/360RA) [Y/n]? ')
|
|
||||||
if confirm.upper() == 'N':
|
|
||||||
self.available_sessions = [SessionType.TV.name]
|
|
||||||
else:
|
|
||||||
self.available_sessions = [SessionType.TV.name]
|
self.available_sessions = [SessionType.TV.name]
|
||||||
|
|
||||||
username, password = None, None
|
while True:
|
||||||
for session_type in self.available_sessions:
|
login_session = None
|
||||||
# create all sessions with the needed API keys
|
|
||||||
if session_type == SessionType.TV.name:
|
|
||||||
sessions[session_type] = TidalTvSession(self.settings['tv_token'], self.settings['tv_secret'])
|
|
||||||
elif session_type == SessionType.MOBILE_ATMOS.name:
|
|
||||||
sessions[session_type] = TidalMobileSession(self.settings['mobile_atmos_token'])
|
|
||||||
else:
|
|
||||||
sessions[session_type] = TidalMobileSession(self.settings['mobile_default_token'])
|
|
||||||
|
|
||||||
if session_type in saved_sessions:
|
def auth_and_save_session(session, session_type):
|
||||||
logging.debug(f'{module_information.service_name}: {session_type} session found, loading')
|
session = self.auth_session(session, session_type, login_session)
|
||||||
|
|
||||||
# load the dictionary from the temporary_settings_controller inside the TidalSession class
|
|
||||||
sessions[session_type].set_storage(saved_sessions[session_type])
|
|
||||||
else:
|
|
||||||
logging.debug(f'{module_information.service_name}: No {session_type} session found, creating new one')
|
|
||||||
if session_type == SessionType.TV.name:
|
|
||||||
self.print(f'{module_information.service_name}: Creating a TV session')
|
|
||||||
sessions[session_type].auth()
|
|
||||||
else:
|
|
||||||
if not username or not password:
|
|
||||||
self.print(f'{module_information.service_name}: Creating a Mobile session')
|
|
||||||
self.print(f'{module_information.service_name}: Enter your TIDAL username and password:')
|
|
||||||
self.print(f'{module_information.service_name}: (password will not be echoed)')
|
|
||||||
username = input(' Username: ')
|
|
||||||
password = getpass(' Password: ')
|
|
||||||
sessions[session_type].auth(username, password)
|
|
||||||
self.print(f'Successfully logged in, using {session_type} token!')
|
|
||||||
|
|
||||||
# get the dict representation from the TidalSession object and save it into saved_session/loginstorage
|
# get the dict representation from the TidalSession object and save it into saved_session/loginstorage
|
||||||
saved_sessions[session_type] = sessions[session_type].get_storage()
|
saved_sessions[session_type] = session.get_storage()
|
||||||
module_controller.temporary_settings_controller.set('sessions', saved_sessions)
|
module_controller.temporary_settings_controller.set('sessions', saved_sessions)
|
||||||
|
return session
|
||||||
|
|
||||||
# always try to refresh session
|
# ask for login if there are no saved sessions
|
||||||
if not sessions[session_type].valid():
|
if not saved_sessions:
|
||||||
sessions[session_type].refresh()
|
login_session_type = None
|
||||||
# Save the refreshed session in the temporary settings
|
if len(self.available_sessions) == 1:
|
||||||
saved_sessions[session_type] = sessions[session_type].get_storage()
|
login_session_type = self.available_sessions[0]
|
||||||
module_controller.temporary_settings_controller.set('sessions', saved_sessions)
|
else:
|
||||||
|
self.print(f'{module_information.service_name}: Choose a login method:')
|
||||||
|
self.print(f'{module_information.service_name}: 1. TV (browser)')
|
||||||
|
self.print(f"{module_information.service_name}: 2. Mobile (username and password, choose TV if this doesn't work)")
|
||||||
|
|
||||||
|
while not login_session_type:
|
||||||
|
input_str = input(' Login method: ')
|
||||||
|
try:
|
||||||
|
login_session_type = {
|
||||||
|
'1': SessionType.TV.name,
|
||||||
|
'tv': SessionType.TV.name,
|
||||||
|
'2': SessionType.MOBILE_DEFAULT.name,
|
||||||
|
'mobile': SessionType.MOBILE_DEFAULT.name,
|
||||||
|
}[input_str.lower()]
|
||||||
|
except KeyError:
|
||||||
|
self.print(f'{module_information.service_name}: Invalid choice, try again')
|
||||||
|
|
||||||
|
login_session = auth_and_save_session(self.init_session(login_session_type), login_session_type)
|
||||||
|
|
||||||
|
for session_type in self.available_sessions:
|
||||||
|
sessions[session_type] = self.init_session(session_type)
|
||||||
|
|
||||||
|
if session_type in saved_sessions:
|
||||||
|
logging.debug(f'{module_information.service_name}: {session_type} session found, loading')
|
||||||
|
|
||||||
|
# load the dictionary from the temporary_settings_controller inside the TidalSession class
|
||||||
|
sessions[session_type].set_storage(saved_sessions[session_type])
|
||||||
|
else:
|
||||||
|
logging.debug(f'{module_information.service_name}: No {session_type} session found, creating new one')
|
||||||
|
sessions[session_type] = auth_and_save_session(sessions[session_type], session_type)
|
||||||
|
|
||||||
|
# always try to refresh session
|
||||||
|
if not sessions[session_type].valid():
|
||||||
|
sessions[session_type].refresh()
|
||||||
|
# Save the refreshed session in the temporary settings
|
||||||
|
saved_sessions[session_type] = sessions[session_type].get_storage()
|
||||||
|
module_controller.temporary_settings_controller.set('sessions', saved_sessions)
|
||||||
|
|
||||||
while True:
|
|
||||||
# check for a valid subscription
|
# check for a valid subscription
|
||||||
subscription = self.check_subscription(sessions[session_type].get_subscription())
|
subscription = self.check_subscription(sessions[session_type].get_subscription())
|
||||||
if subscription:
|
if not subscription:
|
||||||
|
confirm = input(' Do you want to relogin? [Y/n]: ')
|
||||||
|
|
||||||
|
if confirm.upper() == 'N':
|
||||||
|
self.print('Exiting...')
|
||||||
|
exit()
|
||||||
|
|
||||||
|
# reset saved sessions and loop back to login
|
||||||
|
saved_sessions = {}
|
||||||
break
|
break
|
||||||
|
|
||||||
confirm = input(' Do you want to create a new session? [Y/n]: ')
|
if not login_session:
|
||||||
|
login_session = sessions[session_type]
|
||||||
|
|
||||||
if confirm.upper() == 'N':
|
if saved_sessions:
|
||||||
self.print('Exiting...')
|
break
|
||||||
exit()
|
|
||||||
|
|
||||||
# create a new session finally
|
|
||||||
if session_type == SessionType.TV.name:
|
|
||||||
self.print(f'{module_information.service_name}: Recreating a TV session')
|
|
||||||
sessions[session_type].auth()
|
|
||||||
else:
|
|
||||||
self.print(f'{module_information.service_name}: Recreating a Mobile session')
|
|
||||||
self.print(f'{module_information.service_name}: Enter your TIDAL username and password:')
|
|
||||||
self.print(f'{module_information.service_name}: (password will not be echoed)')
|
|
||||||
username = input('Username: ')
|
|
||||||
password = getpass('Password: ')
|
|
||||||
sessions[session_type].auth(username, password)
|
|
||||||
|
|
||||||
saved_sessions[session_type] = sessions[session_type].get_storage()
|
|
||||||
module_controller.temporary_settings_controller.set('sessions', saved_sessions)
|
|
||||||
|
|
||||||
# reset username and password
|
|
||||||
username, password = None, None
|
|
||||||
|
|
||||||
# only needed for region locked albums where the track is available but force_album_format is used
|
# only needed for region locked albums where the track is available but force_album_format is used
|
||||||
self.album_cache = {}
|
self.album_cache = {}
|
||||||
@@ -161,6 +156,39 @@ class ModuleInterface:
|
|||||||
# load the Tidal session with all saved sessions (TV, Mobile Atmos, Mobile Default)
|
# load the Tidal session with all saved sessions (TV, Mobile Atmos, Mobile Default)
|
||||||
self.session: TidalApi = TidalApi(sessions)
|
self.session: TidalApi = TidalApi(sessions)
|
||||||
|
|
||||||
|
def init_session(self, session_type):
|
||||||
|
session = None
|
||||||
|
# initialize session with the needed API keys
|
||||||
|
if session_type == SessionType.TV.name:
|
||||||
|
session = TidalTvSession(self.settings['tv_token'], self.settings['tv_secret'])
|
||||||
|
elif session_type == SessionType.MOBILE_ATMOS.name:
|
||||||
|
session = TidalMobileSession(self.settings['mobile_atmos_token'])
|
||||||
|
else:
|
||||||
|
session = TidalMobileSession(self.settings['mobile_default_token'])
|
||||||
|
return session
|
||||||
|
|
||||||
|
def auth_session(self, session, session_type, login_session):
|
||||||
|
if login_session:
|
||||||
|
# refresh tokens can be used with any client id
|
||||||
|
# this can be used to switch to any client type from an existing session
|
||||||
|
session.refresh_token = login_session.refresh_token
|
||||||
|
session.user_id = login_session.user_id
|
||||||
|
session.country_code = login_session.country_code
|
||||||
|
session.refresh()
|
||||||
|
elif session_type == SessionType.TV.name:
|
||||||
|
self.print(f'{module_information.service_name}: Creating a TV session')
|
||||||
|
session.auth()
|
||||||
|
else:
|
||||||
|
self.print(f'{module_information.service_name}: Creating a Mobile session')
|
||||||
|
self.print(f'{module_information.service_name}: Enter your TIDAL username and password:')
|
||||||
|
self.print(f'{module_information.service_name}: (password will not be echoed)')
|
||||||
|
username = input(' Username: ')
|
||||||
|
password = getpass(' Password: ')
|
||||||
|
session.auth(username, password)
|
||||||
|
self.print(f'Successfully logged in, using {session_type} token!')
|
||||||
|
|
||||||
|
return session
|
||||||
|
|
||||||
def check_subscription(self, subscription: str) -> bool:
|
def check_subscription(self, subscription: str) -> bool:
|
||||||
# returns true if "disable_subscription_checks" is enabled or subscription is HIFI Plus
|
# returns true if "disable_subscription_checks" is enabled or subscription is HIFI Plus
|
||||||
if not self.disable_subscription_check and subscription not in {'HIFI', 'PREMIUM_PLUS'}:
|
if not self.disable_subscription_check and subscription not in {'HIFI', 'PREMIUM_PLUS'}:
|
||||||
@@ -185,10 +213,13 @@ class ModuleInterface:
|
|||||||
return 'https://resources.tidal.com/videos/{0}/{1}x{1}.mp4'.format(cover_id.replace('-', '/'), size)
|
return 'https://resources.tidal.com/videos/{0}/{1}x{1}.mp4'.format(cover_id.replace('-', '/'), size)
|
||||||
|
|
||||||
def search(self, query_type: DownloadTypeEnum, query: str, track_info: TrackInfo = None, limit: int = 20):
|
def search(self, query_type: DownloadTypeEnum, query: str, track_info: TrackInfo = None, limit: int = 20):
|
||||||
results = self.session.get_search_data(query, limit=limit)
|
if track_info and track_info.tags.isrc:
|
||||||
|
results = self.session.get_tracks_by_isrc(track_info.tags.isrc)
|
||||||
|
else:
|
||||||
|
results = self.session.get_search_data(query, limit=limit)[query_type.name + 's']
|
||||||
|
|
||||||
items = []
|
items = []
|
||||||
for i in results[query_type.name + 's'].get('items'):
|
for i in results.get('items'):
|
||||||
duration, name = None, None
|
duration, name = None, None
|
||||||
if query_type is DownloadTypeEnum.artist:
|
if query_type is DownloadTypeEnum.artist:
|
||||||
name = i.get('name')
|
name = i.get('name')
|
||||||
@@ -424,26 +455,43 @@ class ModuleInterface:
|
|||||||
# add the region locked album to the cache in order to properly use it later (force_album_format)
|
# add the region locked album to the cache in order to properly use it later (force_album_format)
|
||||||
self.album_cache = {album_id: album_data}
|
self.album_cache = {album_id: album_data}
|
||||||
|
|
||||||
# check if album is only available in LOSSLESS and STEREO, so it switches to the MOBILE_DEFAULT which will
|
media_tags = track_data['mediaMetadata']['tags']
|
||||||
# get FLACs faster, instead of using MPEG-DASH
|
format = None
|
||||||
# lmao what did I smoke when I wrote this, track_data and not album_data!
|
if 'HIRES_LOSSLESS' in media_tags and quality_tier is QualityEnum.HIFI:
|
||||||
if (self.settings['force_non_spatial'] or (
|
format = 'flac_hires'
|
||||||
(quality_tier is QualityEnum.LOSSLESS or track_data.get('audioQuality') == 'LOSSLESS')
|
if 'SONY_360RA' in media_tags and not format and not self.settings['force_non_spatial']:
|
||||||
and track_data.get('audioModes') == ['STEREO'])) and (
|
format = '360ra'
|
||||||
SessionType.MOBILE_DEFAULT.name in self.available_sessions):
|
if 'DOLBY_ATMOS' in media_tags and not format and not self.settings['force_non_spatial']:
|
||||||
self.session.default = SessionType.MOBILE_DEFAULT
|
if self.settings['prefer_ac4']:
|
||||||
elif (track_data.get('audioModes') == ['SONY_360RA']
|
format = 'ac4'
|
||||||
or ('DOLBY_ATMOS' in track_data.get('audioModes') and self.settings['prefer_ac4'])) \
|
else:
|
||||||
and SessionType.MOBILE_ATMOS.name in self.available_sessions:
|
format = 'ac3'
|
||||||
self.session.default = SessionType.MOBILE_ATMOS
|
|
||||||
|
session = {
|
||||||
|
'flac_hires': SessionType.MOBILE_ATMOS,
|
||||||
|
'360ra': SessionType.MOBILE_ATMOS,
|
||||||
|
'ac4': SessionType.MOBILE_ATMOS,
|
||||||
|
'ac3': SessionType.TV,
|
||||||
|
# MOBILE_DEFAULT is used whenever possible to avoid MPEG-DASH, which slows downloading
|
||||||
|
None: SessionType.MOBILE_DEFAULT,
|
||||||
|
}[format]
|
||||||
|
|
||||||
|
if not format and 'SONY_360RA' in media_tags:
|
||||||
|
# if 360RA is available, we don't use the mobile session here because that will get 360RA
|
||||||
|
# there are no tracks with both 360RA and atmos afaik,
|
||||||
|
# so this shouldn't be an issue for now
|
||||||
|
session = SessionType.TV
|
||||||
|
|
||||||
|
if session.name in self.available_sessions:
|
||||||
|
self.session.default = session
|
||||||
else:
|
else:
|
||||||
self.session.default = SessionType.TV
|
format = None
|
||||||
|
|
||||||
# define all default values in case the stream_data is None (region locked)
|
# define all default values in case the stream_data is None (region locked)
|
||||||
audio_track, mqa_file, track_codec, bitrate, download_args, error = None, None, CodecEnum.FLAC, None, None, None
|
audio_track, mqa_file, track_codec, bitrate, download_args, error = None, None, CodecEnum.FLAC, None, None, None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
stream_data = self.session.get_stream_url(track_id, self.quality_parse[quality_tier])
|
stream_data = self.session.get_stream_url(track_id, self.quality_parse[quality_tier] if format != 'flac_hires' else 'HI_RES_LOSSLESS')
|
||||||
except TidalRequestError as e:
|
except TidalRequestError as e:
|
||||||
error = e
|
error = e
|
||||||
# definitely region locked
|
# definitely region locked
|
||||||
@@ -489,7 +537,8 @@ class ModuleInterface:
|
|||||||
download_args = {'file_url': manifest['urls'][0]}
|
download_args = {'file_url': manifest['urls'][0]}
|
||||||
|
|
||||||
# https://en.wikipedia.org/wiki/Audio_bit_depth#cite_ref-1
|
# https://en.wikipedia.org/wiki/Audio_bit_depth#cite_ref-1
|
||||||
bit_depth = 16 if track_codec in {CodecEnum.FLAC, CodecEnum.ALAC} else None
|
bit_depth = (24 if stream_data and stream_data['audioQuality'] == 'HI_RES_LOSSLESS' else 16) \
|
||||||
|
if track_codec in {CodecEnum.FLAC, CodecEnum.ALAC} else None
|
||||||
sample_rate = 48 if track_codec in {CodecEnum.EAC3, CodecEnum.MHA1, CodecEnum.AC4} else 44.1
|
sample_rate = 48 if track_codec in {CodecEnum.EAC3, CodecEnum.MHA1, CodecEnum.AC4} else 44.1
|
||||||
|
|
||||||
if stream_data:
|
if stream_data:
|
||||||
@@ -498,7 +547,8 @@ class ModuleInterface:
|
|||||||
'LOW': 96,
|
'LOW': 96,
|
||||||
'HIGH': 320,
|
'HIGH': 320,
|
||||||
'LOSSLESS': 1411,
|
'LOSSLESS': 1411,
|
||||||
'HI_RES': None
|
'HI_RES': None,
|
||||||
|
'HI_RES_LOSSLESS': None
|
||||||
}[stream_data['audioQuality']]
|
}[stream_data['audioQuality']]
|
||||||
|
|
||||||
# manually set bitrate for immersive formats
|
# manually set bitrate for immersive formats
|
||||||
@@ -514,6 +564,8 @@ class ModuleInterface:
|
|||||||
# more precise bitrate tidal uses MPEG-DASH
|
# more precise bitrate tidal uses MPEG-DASH
|
||||||
if audio_track:
|
if audio_track:
|
||||||
bitrate = audio_track.bitrate // 1000
|
bitrate = audio_track.bitrate // 1000
|
||||||
|
if stream_data['audioQuality'] == 'HI_RES_LOSSLESS':
|
||||||
|
sample_rate = audio_track.sample_rate / 1000
|
||||||
|
|
||||||
# now set everything for MQA
|
# now set everything for MQA
|
||||||
if mqa_file is not None and mqa_file.is_mqa:
|
if mqa_file is not None and mqa_file.is_mqa:
|
||||||
|
|||||||
20
tidal_api.py
20
tidal_api.py
@@ -13,7 +13,7 @@ import requests
|
|||||||
import urllib3
|
import urllib3
|
||||||
|
|
||||||
import urllib.parse as urlparse
|
import urllib.parse as urlparse
|
||||||
from urllib.parse import parse_qs
|
from urllib.parse import parse_qs, quote
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from utils.utils import create_requests_session
|
from utils.utils import create_requests_session
|
||||||
@@ -110,7 +110,7 @@ class TidalApi(object):
|
|||||||
return resp_json
|
return resp_json
|
||||||
|
|
||||||
def get_stream_url(self, track_id, quality):
|
def get_stream_url(self, track_id, quality):
|
||||||
return self._get('tracks/' + str(track_id) + '/playbackinfopostpaywall', {
|
return self._get('tracks/' + str(track_id) + '/playbackinfopostpaywall/v4', {
|
||||||
'playbackmode': 'STREAM',
|
'playbackmode': 'STREAM',
|
||||||
'assetpresentation': 'FULL',
|
'assetpresentation': 'FULL',
|
||||||
'audioquality': quality,
|
'audioquality': quality,
|
||||||
@@ -174,6 +174,11 @@ class TidalApi(object):
|
|||||||
|
|
||||||
def get_video(self, video_id):
|
def get_video(self, video_id):
|
||||||
return self._get('videos/' + str(video_id))
|
return self._get('videos/' + str(video_id))
|
||||||
|
|
||||||
|
def get_tracks_by_isrc(self, isrc):
|
||||||
|
return self._get('tracks', params={
|
||||||
|
'isrc': isrc
|
||||||
|
})
|
||||||
|
|
||||||
def get_favorite_tracks(self, user_id):
|
def get_favorite_tracks(self, user_id):
|
||||||
return self._get('users/' + str(user_id) + '/favorites/tracks')
|
return self._get('users/' + str(user_id) + '/favorites/tracks')
|
||||||
@@ -323,8 +328,8 @@ class TidalMobileSession(TidalSession):
|
|||||||
self.code_verifier = base64.urlsafe_b64encode(secrets.token_bytes(32)).rstrip(b'=')
|
self.code_verifier = base64.urlsafe_b64encode(secrets.token_bytes(32)).rstrip(b'=')
|
||||||
self.code_challenge = base64.urlsafe_b64encode(hashlib.sha256(self.code_verifier).digest()).rstrip(b'=')
|
self.code_challenge = base64.urlsafe_b64encode(hashlib.sha256(self.code_verifier).digest()).rstrip(b'=')
|
||||||
self.client_unique_key = secrets.token_hex(8)
|
self.client_unique_key = secrets.token_hex(8)
|
||||||
self.user_agent = 'Mozilla/5.0 (Linux; Android 13; Pixel 7 Build/TD1A.221105.001; wv) AppleWebKit/537.36' \
|
self.user_agent = 'Mozilla/5.0 (Linux; Android 13; Pixel 8 Build/TQ2A.230505.002; wv) AppleWebKit/537.36 ' \
|
||||||
'(KHTML, like Gecko) Version/4.0 Chrome/109.0.5414.80 Mobile Safari/537.36'
|
'(KHTML, like Gecko) Version/4.0 Chrome/119.0.6045.163 Mobile Safari/537.36'
|
||||||
|
|
||||||
def auth(self, username: str, password: str):
|
def auth(self, username: str, password: str):
|
||||||
s = requests.Session()
|
s = requests.Session()
|
||||||
@@ -355,10 +360,11 @@ class TidalMobileSession(TidalSession):
|
|||||||
|
|
||||||
# try Tidal DataDome cookie request
|
# try Tidal DataDome cookie request
|
||||||
r = s.post('https://dd.tidal.com/js/', data={
|
r = s.post('https://dd.tidal.com/js/', data={
|
||||||
|
'jsData': f'{{"opts":"endpoint,ajaxListenerPath","ua":"{self.user_agent}"}}',
|
||||||
'ddk': '1F633CDD8EF22541BD6D9B1B8EF13A', # API Key (required)
|
'ddk': '1F633CDD8EF22541BD6D9B1B8EF13A', # API Key (required)
|
||||||
'Referer': r.url, # Referer authorize link (required)
|
'Referer': quote(r.url), # Referer authorize link (required)
|
||||||
'responsePage': 'origin', # useless?
|
'responsePage': 'origin', # useless?
|
||||||
'ddv': '4.4.7' # useless?
|
'ddv': '4.17.0' # useless?
|
||||||
}, headers={
|
}, headers={
|
||||||
'user-agent': self.user_agent,
|
'user-agent': self.user_agent,
|
||||||
'content-type': 'application/x-www-form-urlencoded'
|
'content-type': 'application/x-www-form-urlencoded'
|
||||||
@@ -408,7 +414,7 @@ class TidalMobileSession(TidalSession):
|
|||||||
raise TidalAuthError(r.text)
|
raise TidalAuthError(r.text)
|
||||||
|
|
||||||
# retrieve access code
|
# retrieve access code
|
||||||
r = s.get('https://login.tidal.com/success?lang=en', allow_redirects=False, headers={
|
r = s.get('https://login.tidal.com/success', allow_redirects=False, headers={
|
||||||
'user-agent': self.user_agent,
|
'user-agent': self.user_agent,
|
||||||
'accept-language': 'en-US',
|
'accept-language': 'en-US',
|
||||||
'x-requested-with': 'com.aspiro.tidal'
|
'x-requested-with': 'com.aspiro.tidal'
|
||||||
|
|||||||
Reference in New Issue
Block a user