This commit is contained in:
@@ -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):
|
||||||
|
|||||||
@@ -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."
|
||||||
)
|
)
|
||||||
|
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()
|
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"""
|
||||||
|
|||||||
Reference in New Issue
Block a user