SpotiClub Patch v0.2.0
Some checks failed
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
unknown
2025-12-18 03:13:50 +01:00
parent 6c05cf4915
commit 8a7d0fa3c8
2 changed files with 54 additions and 8 deletions

View File

@@ -356,7 +356,7 @@ class AudioKeyManager(PacketsReceiver, Closeable):
if not isinstance(retry_after, (int, float)): if not isinstance(retry_after, (int, float)):
retry_after = 10 retry_after = 10
print( print(
f"[SpotiClub API] Another client is already using this account. Waiting {int(retry_after)}s before retrying..." f"\n[SpotiClub API] Another client is already using this account. Waiting {int(retry_after)}s before retrying...\n"
) )
self.logger.info( self.logger.info(
"[SpotiClub API] Queued client for user %s; waiting %ds before retry", "[SpotiClub API] Queued client for user %s; waiting %ds before retry",
@@ -368,17 +368,17 @@ class AudioKeyManager(PacketsReceiver, Closeable):
if resp.status_code == 401: if resp.status_code == 401:
print( print(
"[SpotiClub API][BAD_LOGIN] It seems your credentials aren't recognized by the API. Please ensure you have entered them correctly, or contact a DEV if you are absolutely certain of their validity." "\n[SpotiClub API][BAD_LOGIN] It seems your credentials aren't recognized by the API. Please ensure you have entered them correctly, or contact a DEV if you are absolutely certain of their validity."
) )
raise SystemExit(1) raise SystemExit(1)
if resp.status_code != 200: if resp.status_code != 200:
raise RuntimeError(f"[SpotiClub API] Sorry, the API returned the unexpected code {resp.status_code}: {resp.text}") raise RuntimeError(f"\n[SpotiClub API] Sorry, the API returned the unexpected code {resp.status_code}: {resp.text}\n")
data = resp.json() data = resp.json()
key_hex = data.get("key") key_hex = data.get("key")
if not isinstance(key_hex, str): if not isinstance(key_hex, str):
raise RuntimeError("[SpotiClub API] Sorry, API response missing 'key'") raise RuntimeError("\n[SpotiClub API] Sorry, API response missing 'key'\n")
country = data.get("country") country = data.get("country")
if isinstance(country, str): if isinstance(country, str):

View File

@@ -600,12 +600,29 @@ class ApResolver:
""" """
response = requests.get("{}?type={}".format(ApResolver.base_url, response = requests.get("{}?type={}".format(ApResolver.base_url,
service_type)) service_type))
# If ApResolve responds with a non-200, treat this as a clear,
# high-level error instead of bubbling up JSON parsing
# exceptions from HTML error pages.
if response.status_code != 200: if response.status_code != 200:
if response.status_code == 502: if response.status_code == 502:
raise RuntimeError( raise RuntimeError(
f"ApResolve request failed with the following return value: {response.content}. Servers might be down!" "Failed to contact Spotify ApResolve (502). "
"Servers might be down or unreachable."
) )
return response.json() raise RuntimeError(
f"Failed to contact Spotify ApResolve (status {response.status_code}). "
"This is usually a network, DNS, or firewall issue."
)
try:
return response.json()
except ValueError as exc:
# Response wasn't valid JSON; surface a friendly error
# instead of a long JSONDecodeError traceback.
raise RuntimeError(
"Spotify ApResolve returned invalid data. "
"This is likely a temporary server or network problem."
) from exc
@staticmethod @staticmethod
def get_random_of(service_type: str) -> str: def get_random_of(service_type: str) -> str:
@@ -1290,6 +1307,18 @@ class Session(Closeable, MessageListener, SubListener):
for attempt in range(1, max_attempts + 1): for attempt in range(1, max_attempts + 1):
try: try:
# Inform the user about each connection attempt so it is
# visible in the console (e.g. when called from Zotify).
# Only show attempt counters after the first failure; the
# initial attempt is shown without numbering.
if attempt == 1:
connect_msg = "Connecting to Spotify..."
else:
connect_msg = (
f"Connecting to Spotify (attempt {attempt}/{max_attempts})..."
)
self.logger.info(connect_msg)
print(connect_msg)
acc = Session.Accumulator() acc = Session.Accumulator()
# Send ClientHello # Send ClientHello
nonce = Random.get_random_bytes(0x10) nonce = Random.get_random_bytes(0x10)
@@ -1414,6 +1443,12 @@ class Session(Closeable, MessageListener, SubListener):
max_attempts, max_attempts,
exc, exc,
) )
if attempt == 1:
print(f"Connecting to Spotify failed: {exc}")
else:
print(
f"Connecting to Spotify (attempt {attempt}/{max_attempts}) failed: {exc}"
)
# Close current connection; a new access point will be # Close current connection; a new access point will be
# selected on the next attempt. # selected on the next attempt.
if self.connection is not None: if self.connection is not None:
@@ -1430,6 +1465,10 @@ class Session(Closeable, MessageListener, SubListener):
self.logger.info( self.logger.info(
"Retrying connection, new access point: %s", address "Retrying connection, new access point: %s", address
) )
print(
"Retrying connection to Spotify with new access point: "
f"{address} (next attempt {attempt + 1}/{max_attempts})"
)
self.connection = Session.ConnectionHolder.create( self.connection = Session.ConnectionHolder.create(
address, None address, None
) )
@@ -1742,9 +1781,16 @@ class Session(Closeable, MessageListener, SubListener):
else: else:
self.logger.warning("Login5 authentication failed: {}".format(login5_response.error)) self.logger.warning("Login5 authentication failed: {}".format(login5_response.error))
else: else:
self.logger.warning("Login5 request failed with status: {}".format(response.status_code)) # Login5 is best-effort; if it fails (e.g. 403 or
# region restrictions), we silently skip it to
# avoid confusing the user when the main
# connection has already failed.
self.logger.debug(
"Login5 request failed with status: %s", response.status_code
)
except Exception as e: except Exception as e:
self.logger.warning("Failed to authenticate with Login5: {}".format(e)) # Also treat unexpected Login5 issues as debug-only noise.
self.logger.debug("Failed to authenticate with Login5: %s", e)
def get_login5_token(self) -> typing.Union[str, None]: def get_login5_token(self) -> typing.Union[str, None]:
"""Get the Login5 access token if available and not expired""" """Get the Login5 access token if available and not expired"""