From bb1f35c1af879123d7aeb575c047fbe7cd1463a2 Mon Sep 17 00:00:00 2001 From: Justin Donofrio Date: Sat, 21 Dec 2024 12:58:26 -0500 Subject: [PATCH] Add qobuz --- src/onthespot/accounts.py | 94 +------ src/onthespot/api/apple_music.py | 2 +- src/onthespot/api/qobuz.py | 348 ++++++++++++++++++++++++ src/onthespot/api/soundcloud.py | 7 +- src/onthespot/api/tidal.py | 5 +- src/onthespot/cli.py | 1 + src/onthespot/downloader.py | 5 +- src/onthespot/gui/mainui.py | 27 +- src/onthespot/gui/settings.py | 11 +- src/onthespot/parse_item.py | 9 +- src/onthespot/resources/icons/qobuz.png | Bin 0 -> 13619 bytes src/onthespot/search.py | 1 + src/onthespot/web.py | 1 + 13 files changed, 407 insertions(+), 104 deletions(-) create mode 100644 src/onthespot/api/qobuz.py create mode 100644 src/onthespot/resources/icons/qobuz.png diff --git a/src/onthespot/accounts.py b/src/onthespot/accounts.py index 2576255..cee0a0a 100644 --- a/src/onthespot/accounts.py +++ b/src/onthespot/accounts.py @@ -3,6 +3,7 @@ from .api.apple_music import apple_music_login_user, apple_music_get_token from .api.bandcamp import bandcamp_login_user from .api.deezer import deezer_login_user, deezer_get_token +from .api.qobuz import qobuz_login_user, qobuz_get_token from .api.soundcloud import soundcloud_login_user, soundcloud_get_token from .api.spotify import spotify_login_user, spotify_get_token from .api.tidal import tidal_login_user, tidal_get_token @@ -29,93 +30,18 @@ def run(self): if not account['active']: continue - if service == 'apple_music': - if self.gui is True: - self.progress.emit(self.tr('Attempting to create session for\n{0}...').format(account['login']['pltvcid']), True) - - valid_login = apple_music_login_user(account) - if valid_login: - if self.gui is True: - self.progress.emit(self.tr('Session created for\n{0}!').format(account['login']['pltvcid']), True) - continue - else: - if self.gui is True: - self.progress.emit(self.tr('Login failed for \n{0}!').format(account['login']['pltvcid']), True) - sleep(0.5) - continue - - elif service == 'bandcamp': - bandcamp_login_user(account) - continue + if self.gui is True: + self.progress.emit(self.tr('Attempting to create session for\n{0}...').format(account['uuid']), True) - elif service == 'deezer': + valid_login = globals()[f"{service}_login_user"](account) + if valid_login: if self.gui is True: - self.progress.emit(self.tr('Attempting to create session for\n{0}...').format(account['login']['arl'][:30]), True) - try: - if deezer_login_user(account) is True: - if self.gui is True: - self.progress.emit(self.tr('Session created for\n{0}...!').format(account['login']['arl'][:30]), True) - continue - else: - if self.gui is True: - self.progress.emit(self.tr('Login failed for \n{0}...!').format(account['login']['arl'][:30]), True) - continue - except Exception as e: - if self.gui is True: - self.progress.emit(self.tr('Login failed for \n{0}...!').format(account['login']['arl'][:30]), True) - sleep(0.5) - continue - - elif service == 'soundcloud': - if self.gui is True: - self.progress.emit(self.tr('Attempting to create session for\n{0}...').format(account['login']['client_id']), True) - - valid_login = soundcloud_login_user(account) - if valid_login and account['uuid'] == 'public_soundcloud': - if self.gui is True: - self.progress.emit(self.tr('Session created for\n{0}!').format(account['login']['client_id']), True) - continue - else: - if self.gui is True: - self.progress.emit(self.tr('Login failed for \n{0}!').format(account['login']['client_id']), True) - sleep(0.5) - continue - - elif service == 'spotify': - if self.gui is True: - self.progress.emit(self.tr('Attempting to create session for\n{0}...').format(account['login']['username']), True) - try: - if spotify_login_user(account) is True: - if self.gui is True: - self.progress.emit(self.tr('Session created for\n{0}!').format(account['login']['username']), True) - continue - else: - if self.gui is True: - self.progress.emit(self.tr('Login failed for \n{0}!').format(account['login']['username']), True) - continue - except Exception as e: - if self.gui is True: - self.progress.emit(self.tr('Login failed for \n{0}!').format(account['login']['username']), True) - sleep(0.5) - continue - - elif service == 'tidal': + self.progress.emit(self.tr('Session created for\n{0}!').format(account['uuid']), True) + continue + else: if self.gui is True: - self.progress.emit(self.tr('Attempting to create session for\n{0}...').format(account['login']['username']), True) - - valid_login = tidal_login_user(account) - if valid_login: - if self.gui is True: - self.progress.emit(self.tr('Session created for\n{0}!').format(account['login']['username']), True) - continue - else: - if self.gui is True: - self.progress.emit(self.tr('Login failed for \n{0}!').format(account['login']['username']), True) - sleep(0.5) - continue - - elif service == 'youtube': - youtube_login_user(account) + self.progress.emit(self.tr('Login failed for \n{0}!').format(account['uuid']), True) + sleep(0.5) continue self.finished.emit() diff --git a/src/onthespot/api/apple_music.py b/src/onthespot/api/apple_music.py index 8fc5149..9d9abde 100644 --- a/src/onthespot/api/apple_music.py +++ b/src/onthespot/api/apple_music.py @@ -132,7 +132,7 @@ def apple_music_get_search_results(session, search_term, content_types): params['limit'] = config.get("max_search_results") params['types'] = ",".join(search_types) - results = make_call(f"{BASE_URL}/catalog/{session.cookies.get("itua")}/search", params=params, session=session, skip_cache=True) + results = make_call(f'{BASE_URL}/catalog/{session.cookies.get("itua")}/search', params=params, session=session, skip_cache=True) search_results = [] for result in results['results']: diff --git a/src/onthespot/api/qobuz.py b/src/onthespot/api/qobuz.py new file mode 100644 index 0000000..dfd50ea --- /dev/null +++ b/src/onthespot/api/qobuz.py @@ -0,0 +1,348 @@ + +import base64 +from collections import OrderedDict +import hashlib +import re +import time +import uuid +import requests +from ..otsconfig import config +from ..runtimedata import get_logger, account_pool +from ..utils import conv_list_format, make_call + +logger = get_logger("api.qobuz") +BASE_URL = "https://www.qobuz.com/api.json/0.2" +QOBUZ_LOGIN_URL = "https://play.qobuz.com/login" + + +def qobuz_add_account(email, password): + cfg_copy = config.get('accounts').copy() + new_user = { + "uuid": str(uuid.uuid4()), + "service": "qobuz", + "active": True, + "login": { + "email": email, + "password": password, + } + } + cfg_copy.append(new_user) + config.set_('accounts', cfg_copy) + config.update() + + +def qobuz_login_user(account): + try: + session = requests.Session() + response = session.get("https://play.qobuz.com/login") + login_page = response.text + + bundle_url_match = re.search( + r'', + login_page, + ) + if not bundle_url_match: + raise Exception("Could not find bundle URL.") + + bundle_url = bundle_url_match.group(1) + + response = session.get("https://play.qobuz.com" + bundle_url) + bundle = response.text + + app_id_regex = ( + r'production:{api:{appId:"(?P\d{9})",appSecret:"(\w{32})' + ) + seed_timezone_regex = ( + r'[a-z]\.initialSeed\("(?P[\w=]+)",window\.ut' + r"imezone\.(?P[a-z]+)\)" + ) + info_extras_regex = ( + r'name:"\w+/(?P{timezones})",info:"' + r'(?P[\w=]+)",extras:"(?P[\w=]+)"' + ) + + app_id_match = re.search(app_id_regex, bundle) + if app_id_match is None: + raise Exception("Could not find app id.") + + app_id = str(app_id_match.group("app_id")) + + # Get secrets + seed_matches = re.finditer(seed_timezone_regex, bundle) + secrets = OrderedDict() + for match in seed_matches: + seed, timezone = match.group("seed", "timezone") + secrets[timezone] = [seed] + + # Ensure there are enough seeds to manipulate + if len(secrets) < 2: + raise Exception("Not enough secrets found.") + + # Modify the order of seeds + keypairs = list(secrets.items()) + secrets.move_to_end(keypairs[1][0], last=False) + + # Prepare the regex for info and extras + info_extras_regex_full = info_extras_regex.format( + timezones="|".join(timezone.capitalize() for timezone in secrets), + ) + info_extras_matches = re.finditer(info_extras_regex_full, bundle) + for match in info_extras_matches: + timezone, info, extras = match.group("timezone", "info", "extras") + secrets[timezone.lower()] += [info, extras] + + for secret_pair in secrets: + secrets[secret_pair] = base64.standard_b64decode( + "".join(secrets[secret_pair])[:-44], + ).decode("utf-8") + + vals = list(secrets.values()) + if "" in vals: + vals.remove("") + + app_secrets = vals + + login_url = f"{BASE_URL}/user/login" + + params = {} + params['email'] = account['login']['email'] + params['password'] = account['login']['password'] + params['app_id'] = app_id + + login_data = requests.get(login_url, params=params).json() + + account_pool.append({ + "uuid": account['uuid'], + "username": account['login']['email'], + "service": "qobuz", + "status": "active", + "account_type": 'premium', + "bitrate": '1411k', + "login": { + "email": account['login']['email'], + "password": account['login']['password'], + "app_id": app_id, + "app_secrets": app_secrets, + "user_auth_token": login_data['user_auth_token'], + } + }) + return True + + except Exception as e: + logger.error(f"Unknown Exception: {str(e)}") + account_pool.append({ + "uuid": account['uuid'], + "username": account['login']['email'], + "service": "qobuz", + "status": "error", + "account_type": "N/A", + "bitrate": "N/A", + "login": { + "arl": "", + "license_token": "", + "session": "", + } + }) + return False + + +def qobuz_get_token(parsing_index): + user_auth_token = account_pool[parsing_index]['login']["user_auth_token"] + app_id = account_pool[parsing_index]['login']["app_id"] + app_secrets = account_pool[parsing_index]['login']["app_secrets"] + return {"user_auth_token": user_auth_token, "app_id": app_id, "app_secrets": app_secrets} + + +def qobuz_get_search_results(token, search_term, content_types): + headers = {} + headers['X-User-Auth-Token'] = token['user_auth_token'] + headers['X-App-Id'] = token['app_id'] + + params = {} + params['query'] = search_term + params['limit'] = config.get("max_search_results") + + search_results = [] + + if 'track' in content_types: + track_data = make_call(f'{BASE_URL}/track/search', params=params, headers=headers, skip_cache=True) + for track in track_data['tracks']['items']: + if track: + search_results.append({ + 'item_id': track['id'], + 'item_name': track['title'], + 'item_by': track.get('performer', '').get('name', ''), + 'item_type': "track", + 'item_service': "qobuz", + 'item_url': f'https://play.qobuz.com/track/{track['id']}', + 'item_thumbnail_url': track.get("album", {}).get("image", {}).get("small", "") + }) + + if 'album' in content_types: + album_data = make_call(f'{BASE_URL}/album/search', params=params, headers=headers, skip_cache=True) + for album in album_data['albums']['items']: + if album: + search_results.append({ + 'item_id': album['id'], + 'item_name': album['title'], + 'item_by': album.get('artist', '').get('name', ''), + 'item_type': "album", + 'item_service': "qobuz", + 'item_url': f'https://play.qobuz.com/album/{album['id']}', + 'item_thumbnail_url': album.get("image", {}).get("small", '') + }) + + if 'artist' in content_types: + artist_data = make_call(f'{BASE_URL}/artist/search', params=params, headers=headers, skip_cache=True) + for artist in artist_data['artists']['items']: + if artist: + search_results.append({ + 'item_id': artist['id'], + 'item_name': artist.get('name', ''), + 'item_by': artist.get('name', ''), + 'item_type': "artist", + 'item_service': "qobuz", + 'item_url': f'https://play.qobuz.com/artist/{artist['id']}', + 'item_thumbnail_url': artist.get("picture", '') + }) + + if 'playlist' in content_types: + playlist_data = make_call(f'{BASE_URL}/playlist/search', params=params, headers=headers, skip_cache=True) + for playlist in playlist_data.get('playlists', {}).get('items', []): + if playlist: + search_results.append({ + 'item_id': playlist['id'], + 'item_name': playlist.get('name', ''), + 'item_by': playlist.get('owner', '').get('name', 'Qobuz'), + 'item_type': "playlist", + 'item_service': "qobuz", + 'item_url': f'https://play.qobuz.com/playlist/{playlist['id']}', + 'item_thumbnail_url': playlist.get("image_rectangle", [])[0] + }) + + return search_results + + +def qobuz_get_track_metadata(token, item_id): + headers = {} + headers['X-User-Auth-Token'] = token['user_auth_token'] + headers['X-App-Id'] = token['app_id'] + + try: + track_data = make_call(f'{BASE_URL}/track/get?track_id={item_id}', headers=headers) + album_data = make_call(f'{BASE_URL}/album/get?album_id={track_data.get('album', {}).get('id', '')}', headers=headers) + except Exception: + return + + # Artists + artists = [] + for artist in track_data.get('album', {}).get('artists', ''): + artists.append(artist.get('name', '')) + if not artists: + artists = [track_data.get('album', {}).get('artist', {}).get('name', '')] + + # Track Number + track_number = None + for i, track in enumerate(album_data.get('tracks').get('items', [])): + if track.get('id', '') == track_data.get('id', ''): + track_number = i + 1 + break + if not track_number: + track_number = track_data.get('album', {}).get('track_number', '') + + info = {} + info['copyright'] = track_data.get('copyright', '') + info['performers'] = track_data.get('performers', '') + info['album_artists'] = track_data.get('album', {}).get('artist', {}).get('name', '') + info['artists'] = conv_list_format(artists) + + info['image_url'] = track_data.get('album', {}).get('image', {}).get('large', '') + info['upc'] = track_data.get('album', {}).get('upc', '') + info['label'] = track_data.get('album', {}).get('label', {}).get('name', '') + info['album_name'] = track_data.get('album', {}).get('title', '') + info['total_tracks'] = track_data.get('album', {}).get('tracks_count', '') + info['genre'] = conv_list_format(track_data.get('album', {}).get('genres_list', [])[-1].split('→')) + info['release_year'] = track_data.get('album', {}).get('release_date_original', '').split("-")[0] + info['description'] = track_data.get('album', {}).get('description', '') + info['total_discs'] = track_data.get('album', {}).get('media_count', '') + + info['isrc'] = track_data.get('isrc', '') + info['title'] = track_data.get('title', '') + info['length'] = str(track_data.get('duration', '')) + '000' + #info['track_number'] = track_data.get('track_number', '') + info['track_number'] = track_number + info['disc_number'] = track_data.get('media_number', '') + info['is_playable'] = track_data.get('streamable', '') + info['item_url'] = f'https://play.qobuz.com/track/{item_id}' + + return info + + +def qobuz_get_album_track_ids(token, album_id): + logger.info(f"Getting tracks from album: {album_id}") + headers = {} + headers['X-User-Auth-Token'] = token['user_auth_token'] + headers['X-App-Id'] = token['app_id'] + + album_data = make_call(f'{BASE_URL}/album/get?album_id={album_id}', headers=headers) + + item_ids = [] + for track in album_data.get('tracks', {}).get('items', []): + item_ids.append(track['id']) + return item_ids + + +def qobuz_get_artist_album_ids(token, album_id): + logger.info(f"Getting tracks from album: {album_id}") + headers = {} + headers['X-User-Auth-Token'] = token['user_auth_token'] + headers['X-App-Id'] = token['app_id'] + + album_data = make_call(f'{BASE_URL}/artist/page?artist_id={album_id}', headers=headers) + + item_ids = [] + for album_type in album_data.get('releases', []): + for album in album_type.get('items', []): + item_ids.append(album['id']) + return item_ids + + +def qobuz_get_playlist_data(token, playlist_id): + logger.info(f"Get playlist data for playlist: {playlist_id}") + headers = {} + headers['X-User-Auth-Token'] = token['user_auth_token'] + headers['X-App-Id'] = token['app_id'] + + playlist_data = make_call(f'{BASE_URL}/playlist/get?playlist_id={playlist_id}&extra=track_ids', headers=headers, skip_cache=True) + + playlist_name = playlist_data.get('name', '') + playlist_by = playlist_data.get('owner', {}).get('name', 'Qobuz') + track_ids = playlist_data.get('track_ids', []) + return playlist_name, playlist_by, track_ids + + +def qobuz_get_file_url(token, item_id): + headers = {} + headers['X-User-Auth-Token'] = token['user_auth_token'] + headers['X-App-Id'] = token['app_id'] + + for secret in token['app_secrets']: + quality = 27 + intent = 'stream' + + # Create the signature for the request + unix_ts = int(time.time()) + r_sig = f"trackgetFileUrlformat_id{quality}intent{intent}track_id{item_id}{unix_ts}{secret}" # Replace with your secret + r_sig_hashed = hashlib.md5(r_sig.encode("utf-8")).hexdigest() + + params = {} + params['request_ts'] = unix_ts + params['request_sig'] = r_sig_hashed + params['track_id'] = item_id + params['format_id'] = quality + params['intent'] = intent + + file_response = requests.get(f"{BASE_URL}/track/getFileUrl", params=params, headers=headers) + if file_response.status_code == 200: + file_data = file_response.json() + return file_data.get("url") diff --git a/src/onthespot/api/soundcloud.py b/src/onthespot/api/soundcloud.py index 5d90115..34a2648 100644 --- a/src/onthespot/api/soundcloud.py +++ b/src/onthespot/api/soundcloud.py @@ -125,10 +125,9 @@ def soundcloud_add_account(): def soundcloud_get_token(parsing_index): - accounts = config.get("accounts") - client_id = accounts[parsing_index]['login']["client_id"] - app_version = accounts[parsing_index]['login']["app_version"] - app_locale = accounts[parsing_index]['login']["app_locale"] + client_id = account_pool[parsing_index]['login']["client_id"] + app_version = account_pool[parsing_index]['login']["app_version"] + app_locale = account_pool[parsing_index]['login']["app_locale"] return {"client_id": client_id, "app_version": app_version, "app_locale": app_locale} diff --git a/src/onthespot/api/tidal.py b/src/onthespot/api/tidal.py index 89ba4c9..e09e3a5 100644 --- a/src/onthespot/api/tidal.py +++ b/src/onthespot/api/tidal.py @@ -132,9 +132,8 @@ def tidal_login_user(account): def tidal_get_token(parsing_index): - accounts = config.get("accounts") - access_token = accounts[parsing_index]['login']["access_token"] - country_code = accounts[parsing_index]['login']["country_code"] + access_token = account_pool[parsing_index]['login']["access_token"] + country_code = account_pool[parsing_index]['login']["country_code"] return {"access_token": access_token, "country_code": country_code} diff --git a/src/onthespot/cli.py b/src/onthespot/cli.py index 9ba9ec6..135dc23 100644 --- a/src/onthespot/cli.py +++ b/src/onthespot/cli.py @@ -11,6 +11,7 @@ from .api.apple_music import apple_music_get_track_metadata from .api.bandcamp import bandcamp_get_track_metadata from .api.deezer import deezer_get_track_metadata, deezer_add_account +from .api.qobuz import qobuz_get_track_metadata from .api.soundcloud import soundcloud_get_track_metadata from .api.spotify import MirrorSpotifyPlayback, spotify_new_session, spotify_get_track_metadata, spotify_get_episode_metadata from .api.tidal import tidal_get_track_metadata diff --git a/src/onthespot/downloader.py b/src/onthespot/downloader.py index 262a6ab..22cde32 100644 --- a/src/onthespot/downloader.py +++ b/src/onthespot/downloader.py @@ -13,6 +13,7 @@ from .api.apple_music import apple_music_get_track_metadata, apple_music_get_decryption_key, apple_music_get_lyrics, apple_music_get_webplayback_info from .api.bandcamp import bandcamp_get_track_metadata from .api.deezer import deezer_get_track_metadata, get_song_info_from_deezer_website, genurlkey, calcbfkey, decryptfile +from .api.qobuz import qobuz_get_track_metadata, qobuz_get_file_url from .api.soundcloud import soundcloud_get_track_metadata from .api.spotify import spotify_get_track_metadata, spotify_get_episode_metadata, spotify_get_lyrics from .api.tidal import tidal_get_track_metadata, tidal_get_lyrics, tidal_get_file_url @@ -379,10 +380,10 @@ def run(self): if self.gui: self.progress.emit(item, self.tr("Downloading"), int((downloaded / total_size) * 100)) - elif item_service == "tidal": + elif item_service in ("qobuz", "tidal"): default_format = '.flac' bitrate = "1411k" - file_url = tidal_get_file_url(token, item_id) + file_url = globals()[f"{item_service}_get_file_url"](token, item_id) response = requests.get(file_url, stream=True) total_size = int(response.headers.get('Content-Length', 0)) downloaded = 0 diff --git a/src/onthespot/gui/mainui.py b/src/onthespot/gui/mainui.py index ae53a89..093d349 100644 --- a/src/onthespot/gui/mainui.py +++ b/src/onthespot/gui/mainui.py @@ -11,6 +11,7 @@ from ..api.apple_music import apple_music_add_account, apple_music_get_track_metadata from ..api.bandcamp import bandcamp_add_account, bandcamp_get_track_metadata from ..api.deezer import deezer_add_account, deezer_get_track_metadata +from ..api.qobuz import qobuz_add_account, qobuz_get_track_metadata from ..api.soundcloud import soundcloud_add_account, soundcloud_get_token, soundcloud_get_track_metadata from ..api.spotify import MirrorSpotifyPlayback, spotify_get_token, spotify_get_track_metadata, spotify_get_episode_metadata, spotify_new_session from ..api.tidal import tidal_add_account_pt1, tidal_add_account_pt2, tidal_get_track_metadata @@ -627,8 +628,26 @@ def set_login_fields(self): self.inp_login_password.clear() ) - # Soundcloud + # Qobuz elif self.inp_login_service.currentIndex() == 3: + self.inp_login_password.setDisabled(False) + self.lb_login_username.show() + self.lb_login_username.setText(self.tr("Email")) + self.inp_login_username.show() + self.lb_login_password.setText(self.tr("Password")) + self.lb_login_password.show() + self.inp_login_password.show() + self.btn_login_add.clicked.disconnect() + self.btn_login_add.show() + self.btn_login_add.setIcon(QIcon()) + self.btn_login_add.setText(self.tr("Add Account")) + self.btn_login_add.clicked.connect(lambda: + (self.__show_popup_dialog(self.tr("Account added, please restart the app.")) or True) and + qobuz_add_account(self.inp_login_username.text(), self.inp_login_password.text()) + ) + + # Soundcloud + elif self.inp_login_service.currentIndex() == 4: self.inp_login_password.setDisabled(False) self.lb_login_username.hide() self.inp_login_username.hide() @@ -650,7 +669,7 @@ def set_login_fields(self): ) # Spotify - elif self.inp_login_service.currentIndex() == 4: + elif self.inp_login_service.currentIndex() == 5: self.inp_login_password.setDisabled(False) self.lb_login_username.hide() self.inp_login_username.hide() @@ -667,7 +686,7 @@ def set_login_fields(self): self.btn_login_add.clicked.connect(self.add_spotify_account) # Tidal - elif self.inp_login_service.currentIndex() == 5: + elif self.inp_login_service.currentIndex() == 6: self.inp_login_password.setDisabled(False) self.lb_login_username.hide() self.inp_login_username.hide() @@ -680,7 +699,7 @@ def set_login_fields(self): self.btn_login_add.clicked.connect(self.add_tidal_account) # Youtube - elif self.inp_login_service.currentIndex() == 6: + elif self.inp_login_service.currentIndex() == 7: self.inp_login_password.setDisabled(False) self.lb_login_username.hide() self.inp_login_username.hide() diff --git a/src/onthespot/gui/settings.py b/src/onthespot/gui/settings.py index 7f2eba3..e7207b4 100644 --- a/src/onthespot/gui/settings.py +++ b/src/onthespot/gui/settings.py @@ -31,11 +31,12 @@ def load_config(self): self.inp_login_service.insertItem(0, self.get_icon('apple_music'), "") self.inp_login_service.insertItem(1, self.get_icon('bandcamp'), "") self.inp_login_service.insertItem(2, self.get_icon('deezer'), "") - self.inp_login_service.insertItem(3, self.get_icon('soundcloud'), "") - self.inp_login_service.insertItem(4, self.get_icon('spotify'), "") - self.inp_login_service.insertItem(5, self.get_icon('tidal'), "") - self.inp_login_service.insertItem(6, self.get_icon('youtube'), "") - self.inp_login_service.setCurrentIndex(4) + self.inp_login_service.insertItem(3, self.get_icon('qobuz'), "") + self.inp_login_service.insertItem(4, self.get_icon('soundcloud'), "") + self.inp_login_service.insertItem(5, self.get_icon('spotify'), "") + self.inp_login_service.insertItem(6, self.get_icon('tidal'), "") + self.inp_login_service.insertItem(7, self.get_icon('youtube'), "") + self.inp_login_service.setCurrentIndex(5) #self.btn_reset_config.setIcon(self.get_icon('trash')) self.btn_save_config.setIcon(self.get_icon('save')) diff --git a/src/onthespot/parse_item.py b/src/onthespot/parse_item.py index 3544fe2..1e03cce 100755 --- a/src/onthespot/parse_item.py +++ b/src/onthespot/parse_item.py @@ -4,6 +4,7 @@ from .api.apple_music import apple_music_get_album_track_ids, apple_music_get_artist_album_ids, apple_music_get_playlist_data from .api.bandcamp import bandcamp_get_album_track_ids, bandcamp_get_artist_album_ids from .api.deezer import deezer_get_album_track_ids, deezer_get_artist_album_ids, deezer_get_playlist_data +from .api.qobuz import qobuz_get_album_track_ids, qobuz_get_artist_album_ids, qobuz_get_playlist_data from .api.soundcloud import soundcloud_parse_url, soundcloud_get_set_items from .api.spotify import spotify_get_album_track_ids, spotify_get_artist_album_ids, spotify_get_playlist_items, spotify_get_playlist_data, spotify_get_liked_songs, spotify_get_your_episodes, spotify_get_show_episode_ids from .api.tidal import tidal_get_album_track_ids, tidal_get_artist_album_ids, tidal_get_playlist_data, tidal_get_mix_data @@ -15,11 +16,11 @@ APPLE_MUSIC_URL_REGEX = re.compile(r'https?://music\.apple\.com/([a-z]{2})/(?Palbum|playlist|artist)/(?P[-a-z0-9]+)/(?P<id>[\w.]+)(?:\?i=(?P<track_id>\d+))?') BANDCAMP_URL_REGEX = re.compile(r'https?://[a-z0-9-]+\.bandcamp\.com(?:/(?P<type>track|album|music)/[a-z0-9-]+)?') DEEZER_URL_REGEX = re.compile(r'https?://www.deezer.com/(?:[a-z]{2}/)?(?P<type>album|playlist|track|artist)/(?P<id>\d+)') +QOBUZ_URL_REGEX = re.compile(r"https?://(open\.|play\.)?qobuz.com/(?:[a-z]{2}-[a-z]{2}/)?(?P<type>album|track|artist|playlist|label)/(?P<id>[-a-z0-9]+)") SOUNDCLOUD_URL_REGEX = re.compile(r"https?://soundcloud.com/[-\w:/]+") SPOTIFY_URL_REGEX = re.compile(r"https?://open.spotify.com/(intl-([a-zA-Z]+)/|)(?P<type>track|album|artist|playlist|episode|show)/(?P<id>[0-9a-zA-Z]{22})(\?si=.+?)?$") TIDAL_URL_REGEX = re.compile(r"https?://(www\.|listen\.)?tidal.com/(browse/)?(?P<type>album|track|artist|playlist|mix)/(?P<id>[-a-z0-9]+)") YOUTUBE_URL_REGEX = re.compile(r"https?://(www\.|music\.)?youtube\.com/(watch\?v=(?P<video_id>[a-zA-Z0-9_-]+)|channel/(?P<channel_id>[a-zA-Z0-9_-]+)|playlist\?list=(?P<playlist_id>[a-zA-Z0-9_-]+))") -#QOBUZ_INTERPRETER_URL_REGEX = re.compile(r"https?://www\.qobuz\.com/\w\w-\w\w/interpreter/[-\w]+/([-\w]+)") def parse_url(url): @@ -47,6 +48,12 @@ def parse_url(url): item_type = match.group("type") item_service = 'deezer' + elif re.match(QOBUZ_URL_REGEX, url): + match = re.search(QOBUZ_URL_REGEX, url) + item_id = match.group("id") + item_type = match.group("type") + item_service = 'qobuz' + elif re.match(SOUNDCLOUD_URL_REGEX, url): token = get_account_token('soundcloud') item_type, item_id = soundcloud_parse_url(url, token) diff --git a/src/onthespot/resources/icons/qobuz.png b/src/onthespot/resources/icons/qobuz.png new file mode 100644 index 0000000000000000000000000000000000000000..852de97207453c211942bb00d481be12e0bd3919 GIT binary patch literal 13619 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 D*+%I% literal 0 HcmV?d00001 diff --git a/src/onthespot/search.py b/src/onthespot/search.py index c8be5dc..9656066 100644 --- a/src/onthespot/search.py +++ b/src/onthespot/search.py @@ -3,6 +3,7 @@ from .api.apple_music import apple_music_get_search_results from .api.bandcamp import bandcamp_get_search_results from .api.deezer import deezer_get_search_results +from .api.qobuz import qobuz_get_search_results from .api.soundcloud import soundcloud_get_search_results from .api.spotify import spotify_get_search_results from .api.tidal import tidal_get_search_results diff --git a/src/onthespot/web.py b/src/onthespot/web.py index 7765f70..701a712 100644 --- a/src/onthespot/web.py +++ b/src/onthespot/web.py @@ -9,6 +9,7 @@ from .api.apple_music import apple_music_get_track_metadata from .api.bandcamp import bandcamp_get_track_metadata from .api.deezer import deezer_get_track_metadata, deezer_add_account +from .api.qobuz import qobuz_get_track_metadata from .api.soundcloud import soundcloud_get_track_metadata from .api.spotify import MirrorSpotifyPlayback, spotify_new_session, spotify_get_track_metadata, spotify_get_episode_metadata from .api.tidal import tidal_get_track_metadata