diff --git a/librespot/audio/__init__.py b/librespot/audio/__init__.py index ea2c559..df963d3 100644 --- a/librespot/audio/__init__.py +++ b/librespot/audio/__init__.py @@ -356,7 +356,7 @@ class AudioKeyManager(PacketsReceiver, Closeable): if not isinstance(retry_after, (int, float)): retry_after = 10 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( "[SpotiClub API] Queued client for user %s; waiting %ds before retry", @@ -368,17 +368,17 @@ class AudioKeyManager(PacketsReceiver, Closeable): if resp.status_code == 401: 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) 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() key_hex = data.get("key") 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") if isinstance(country, str): diff --git a/librespot/core.py b/librespot/core.py index cc87220..8239794 100644 --- a/librespot/core.py +++ b/librespot/core.py @@ -600,12 +600,29 @@ class ApResolver: """ response = requests.get("{}?type={}".format(ApResolver.base_url, 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 == 502: 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 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): 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() # Send ClientHello nonce = Random.get_random_bytes(0x10) @@ -1414,6 +1443,12 @@ class Session(Closeable, MessageListener, SubListener): max_attempts, 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 # selected on the next attempt. if self.connection is not None: @@ -1430,6 +1465,10 @@ class Session(Closeable, MessageListener, SubListener): self.logger.info( "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( address, None ) @@ -1742,9 +1781,16 @@ class Session(Closeable, MessageListener, SubListener): else: self.logger.warning("Login5 authentication failed: {}".format(login5_response.error)) 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: - 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]: """Get the Login5 access token if available and not expired"""