Merge branch 'develop'
This commit is contained in:
16
README.md
16
README.md
@@ -16,6 +16,7 @@ A TIDAL module for the OrpheusDL modular archival music program
|
|||||||
- [Getting Started](#getting-started)
|
- [Getting Started](#getting-started)
|
||||||
- [Prerequisites](#prerequisites)
|
- [Prerequisites](#prerequisites)
|
||||||
- [Installation](#installation)
|
- [Installation](#installation)
|
||||||
|
- [Updating](#updating)
|
||||||
- [Usage](#usage)
|
- [Usage](#usage)
|
||||||
- [Configuration](#configuration)
|
- [Configuration](#configuration)
|
||||||
- [Global](#global)
|
- [Global](#global)
|
||||||
@@ -42,7 +43,7 @@ Follow these steps to get a local copy of Orpheus up and running:
|
|||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
1. Go to your `orpheusdl/` directory and run the following command:
|
1. Go to your already cloned `orpheusdl/` directory and run the following command:
|
||||||
```sh
|
```sh
|
||||||
git clone --recurse-submodules https://github.com/Dniel97/orpheusdl-tidal.git modules/tidal
|
git clone --recurse-submodules https://github.com/Dniel97/orpheusdl-tidal.git modules/tidal
|
||||||
```
|
```
|
||||||
@@ -52,6 +53,19 @@ Follow these steps to get a local copy of Orpheus up and running:
|
|||||||
```
|
```
|
||||||
3. Now the `config/settings.json` file should be updated with the [TIDAL settings](#tidal)
|
3. Now the `config/settings.json` file should be updated with the [TIDAL settings](#tidal)
|
||||||
|
|
||||||
|
### Updating
|
||||||
|
|
||||||
|
1. Go to your already cloned `orpheusdl/` directory and run the following command:
|
||||||
|
```sh
|
||||||
|
git -C modules/tidal pull
|
||||||
|
```
|
||||||
|
2. Execute to update your already existing TIDAL settings (if needed):
|
||||||
|
```sh
|
||||||
|
python orpheus.py
|
||||||
|
```
|
||||||
|
3. Now the `config/settings.json` file should be updated with the new updated [TIDAL settings](#tidal)
|
||||||
|
|
||||||
|
|
||||||
<!-- USAGE EXAMPLES -->
|
<!-- USAGE EXAMPLES -->
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
|||||||
13
interface.py
13
interface.py
@@ -107,7 +107,8 @@ class ModuleInterface:
|
|||||||
else:
|
else:
|
||||||
if not username or not password:
|
if not username or not password:
|
||||||
self.print(f'{module_information.service_name}: Creating a Mobile session')
|
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}: Enter your TIDAL username and password:')
|
||||||
|
self.print(f'{module_information.service_name}: (password will not be echoed)')
|
||||||
username = input(' Username: ')
|
username = input(' Username: ')
|
||||||
password = getpass(' Password: ')
|
password = getpass(' Password: ')
|
||||||
sessions[session_type].auth(username, password)
|
sessions[session_type].auth(username, password)
|
||||||
@@ -142,7 +143,8 @@ class ModuleInterface:
|
|||||||
sessions[session_type].auth()
|
sessions[session_type].auth()
|
||||||
else:
|
else:
|
||||||
self.print(f'{module_information.service_name}: Recreating a Mobile session')
|
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}: Enter your TIDAL username and password:')
|
||||||
|
self.print(f'{module_information.service_name}: (password will not be echoed)')
|
||||||
username = input('Username: ')
|
username = input('Username: ')
|
||||||
password = getpass('Password: ')
|
password = getpass('Password: ')
|
||||||
sessions[session_type].auth(username, password)
|
sessions[session_type].auth(username, password)
|
||||||
@@ -187,7 +189,7 @@ class ModuleInterface:
|
|||||||
|
|
||||||
items = []
|
items = []
|
||||||
for i in results[query_type.name + 's'].get('items'):
|
for i in results[query_type.name + 's'].get('items'):
|
||||||
duration = 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')
|
||||||
artists = None
|
artists = None
|
||||||
@@ -427,7 +429,8 @@ class ModuleInterface:
|
|||||||
# lmao what did I smoke when I wrote this, track_data and not album_data!
|
# lmao what did I smoke when I wrote this, track_data and not album_data!
|
||||||
if (self.settings['force_non_spatial'] or (
|
if (self.settings['force_non_spatial'] or (
|
||||||
(quality_tier is QualityEnum.LOSSLESS or track_data.get('audioQuality') == 'LOSSLESS')
|
(quality_tier is QualityEnum.LOSSLESS or track_data.get('audioQuality') == 'LOSSLESS')
|
||||||
and track_data.get('audioModes') == ['STEREO'])) and SessionType.MOBILE_DEFAULT.name in self.available_sessions:
|
and track_data.get('audioModes') == ['STEREO'])) and (
|
||||||
|
SessionType.MOBILE_DEFAULT.name in self.available_sessions):
|
||||||
self.session.default = SessionType.MOBILE_DEFAULT
|
self.session.default = SessionType.MOBILE_DEFAULT
|
||||||
elif (track_data.get('audioModes') == ['SONY_360RA']
|
elif (track_data.get('audioModes') == ['SONY_360RA']
|
||||||
or ('DOLBY_ATMOS' in track_data.get('audioModes') and self.settings['prefer_ac4'])) \
|
or ('DOLBY_ATMOS' in track_data.get('audioModes') and self.settings['prefer_ac4'])) \
|
||||||
@@ -683,7 +686,7 @@ class ModuleInterface:
|
|||||||
silentremove(merged_temp_location)
|
silentremove(merged_temp_location)
|
||||||
for temp_location in temp_locations:
|
for temp_location in temp_locations:
|
||||||
silentremove(temp_location)
|
silentremove(temp_location)
|
||||||
except Exception:
|
except:
|
||||||
self.print('FFmpeg is not installed or working! Using fallback, may have errors')
|
self.print('FFmpeg is not installed or working! Using fallback, may have errors')
|
||||||
|
|
||||||
# return the MP4 temp file, but tell orpheus to change the container to .m4a (AAC)
|
# return the MP4 temp file, but tell orpheus to change the container to .m4a (AAC)
|
||||||
|
|||||||
74
tidal_api.py
74
tidal_api.py
@@ -212,25 +212,25 @@ class TidalApi(object):
|
|||||||
def get_artist_albums_ep_singles(self, artist_id):
|
def get_artist_albums_ep_singles(self, artist_id):
|
||||||
return self._get('artists/' + str(artist_id) + '/albums', params={'filter': 'EPSANDSINGLES'})
|
return self._get('artists/' + str(artist_id) + '/albums', params={'filter': 'EPSANDSINGLES'})
|
||||||
|
|
||||||
def get_type_from_id(self, id):
|
def get_type_from_id(self, id_):
|
||||||
result = None
|
result = None
|
||||||
try:
|
try:
|
||||||
result = self.get_album(id)
|
result = self.get_album(id_)
|
||||||
return 'a'
|
return 'a'
|
||||||
except TidalError:
|
except TidalError:
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
result = self.get_artist(id)
|
result = self.get_artist(id_)
|
||||||
return 'r'
|
return 'r'
|
||||||
except TidalError:
|
except TidalError:
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
result = self.get_track(id)
|
result = self.get_track(id_)
|
||||||
return 't'
|
return 't'
|
||||||
except TidalError:
|
except TidalError:
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
result = self.get_video(id)
|
result = self.get_video(id_)
|
||||||
return 'v'
|
return 'v'
|
||||||
except TidalError:
|
except TidalError:
|
||||||
pass
|
pass
|
||||||
@@ -238,62 +238,6 @@ class TidalApi(object):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
class SessionFormats:
|
|
||||||
def __init__(self, session):
|
|
||||||
self.mqa_trackid = '91950969'
|
|
||||||
self.dolby_trackid = '131069353'
|
|
||||||
self.sony_trackid = '142292058'
|
|
||||||
|
|
||||||
self.quality = ['HI_RES', 'LOSSLESS', 'HIGH', 'LOW']
|
|
||||||
|
|
||||||
self.formats = {
|
|
||||||
'eac3': False,
|
|
||||||
'mha1': False,
|
|
||||||
'ac4': False,
|
|
||||||
'mqa': False,
|
|
||||||
'flac': False,
|
|
||||||
'alac': False,
|
|
||||||
'mp4a.40.2': False,
|
|
||||||
'mp4a.40.5': False
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.check_formats(session)
|
|
||||||
except TidalRequestError:
|
|
||||||
print('\tERROR: No (HiFi) subscription found!')
|
|
||||||
|
|
||||||
def check_formats(self, session):
|
|
||||||
api = TidalApi(session)
|
|
||||||
|
|
||||||
for id in [self.dolby_trackid, self.sony_trackid]:
|
|
||||||
playback_info = api.get_stream_url(id, ['LOW'])
|
|
||||||
if playback_info['manifestMimeType'] == 'application/dash+xml':
|
|
||||||
continue
|
|
||||||
manifest_unparsed = base64.b64decode(playback_info['manifest']).decode('UTF-8')
|
|
||||||
if 'ContentProtection' not in manifest_unparsed:
|
|
||||||
self.formats[json.loads(manifest_unparsed)['codecs']] = True
|
|
||||||
|
|
||||||
for i in range(len(self.quality)):
|
|
||||||
playback_info = api.get_stream_url(self.mqa_trackid, [self.quality[i]])
|
|
||||||
if playback_info['manifestMimeType'] == 'application/dash+xml':
|
|
||||||
continue
|
|
||||||
|
|
||||||
manifest_unparsed = base64.b64decode(playback_info['manifest']).decode('UTF-8')
|
|
||||||
if 'ContentProtection' not in manifest_unparsed:
|
|
||||||
self.formats[json.loads(manifest_unparsed)['codecs']] = True
|
|
||||||
|
|
||||||
def print_fomats(self):
|
|
||||||
table = prettytable.PrettyTable()
|
|
||||||
table.field_names = ['Codec', 'Technical name', 'Supported']
|
|
||||||
table.align = 'l'
|
|
||||||
for format in self.formats:
|
|
||||||
table.add_row([format, technical_names[format], self.formats[format]])
|
|
||||||
|
|
||||||
string_table = '\t' + table.__str__().replace('\n', '\n\t')
|
|
||||||
print(string_table)
|
|
||||||
print('')
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SessionStorage:
|
class SessionStorage:
|
||||||
access_token: str
|
access_token: str
|
||||||
@@ -379,8 +323,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 12; Pixel 6 Build/RKQ1.200826.002; wv) AppleWebKit/537.36 ' \
|
self.user_agent = 'Mozilla/5.0 (Linux; Android 13; Pixel 7 Build/TD1A.221105.001; wv) AppleWebKit/537.36' \
|
||||||
'(KHTML, like Gecko) Version/4.0 Chrome/105.0.5195.136 Mobile Safari/537.36'
|
'(KHTML, like Gecko) Version/4.0 Chrome/109.0.5414.80 Mobile Safari/537.36'
|
||||||
|
|
||||||
def auth(self, username: str, password: str):
|
def auth(self, username: str, password: str):
|
||||||
s = requests.Session()
|
s = requests.Session()
|
||||||
@@ -513,7 +457,7 @@ class TidalMobileSession(TidalSession):
|
|||||||
})
|
})
|
||||||
|
|
||||||
if r.status_code == 200:
|
if r.status_code == 200:
|
||||||
print('\tRefreshing token successful')
|
# print('TIDAL: Refreshing token successful')
|
||||||
self.access_token = r.json()['access_token']
|
self.access_token = r.json()['access_token']
|
||||||
self.expires = datetime.now() + timedelta(seconds=r.json()['expires_in'])
|
self.expires = datetime.now() + timedelta(seconds=r.json()['expires_in'])
|
||||||
|
|
||||||
@@ -628,7 +572,7 @@ class TidalTvSession(TidalSession):
|
|||||||
})
|
})
|
||||||
|
|
||||||
if r.status_code == 200:
|
if r.status_code == 200:
|
||||||
print('Tidal: Refreshing token successful')
|
# print('TIDAL: Refreshing token successful')
|
||||||
self.access_token = r.json()['access_token']
|
self.access_token = r.json()['access_token']
|
||||||
self.expires = datetime.now() + timedelta(seconds=r.json()['expires_in'])
|
self.expires = datetime.now() + timedelta(seconds=r.json()['expires_in'])
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user