Prepare V0.2

This commit is contained in:
unknown
2025-12-18 20:31:56 +01:00
parent 7054dc1dfe
commit e4aed25db8
2 changed files with 105 additions and 83 deletions

View File

@@ -23,6 +23,7 @@ import urllib.parse
import os import os
import json import json
import requests import requests
import atexit
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from librespot.core import Session from librespot.core import Session
@@ -52,6 +53,33 @@ spoticlub_client_serial: typing.Optional[str] = None
spoticlub_loaded_logged: bool = False spoticlub_loaded_logged: bool = False
######################################## ########################################
### SPOTICLUB CLIENT SERIAL TRACKING (DO NOT EDIT) ###
spoticlub_client_serial: typing.Optional[str] = None
spoticlub_loaded_logged: bool = False
def _spoticlub_notify_session_done() -> None:
global spoticlub_user, spoticlub_password, spoticlub_client_serial
try:
if not server_url or not spoticlub_user or not spoticlub_client_serial:
return
base_url = server_url.rsplit("/", 1)[0]
url = base_url + "/client_done"
payload = {
"user": spoticlub_user,
"password": spoticlub_password,
"client_serial": spoticlub_client_serial,
}
requests.post(url, json=payload, timeout=5)
except Exception:
AudioKeyManager.logger.debug(
"[SpotiClub API] Failed to notify server of session completion",
exc_info=True,
)
atexit.register(_spoticlub_notify_session_done)
########################################
class LoadedStream(GeneralAudioStream): class LoadedStream(GeneralAudioStream):
def __init__(self, data: bytes): def __init__(self, data: bytes):
super().__init__() super().__init__()
@@ -327,17 +355,6 @@ class AudioKeyManager(PacketsReceiver, Closeable):
spoticlub_loaded_logged = True spoticlub_loaded_logged = True
print(f"\n[SpotiClub API] Plugin Loaded! Welcome {spoticlub_user}\n") print(f"\n[SpotiClub API] Plugin Loaded! Welcome {spoticlub_user}\n")
# Try to show a Zotify loader while we fetch the remote audio key.
# The import is done lazily here to avoid hard circular imports.
loader = None
try:
from zotify.loader import Loader # type: ignore
from zotify.termoutput import PrintChannel # type: ignore
loader = Loader(PrintChannel.PROGRESS_INFO, "Fetching audio key...")
loader.start()
except Exception:
loader = None
payload = { payload = {
"gid": util.bytes_to_hex(gid), "gid": util.bytes_to_hex(gid),
"file_id": util.bytes_to_hex(file_id), "file_id": util.bytes_to_hex(file_id),
@@ -350,14 +367,21 @@ class AudioKeyManager(PacketsReceiver, Closeable):
tries = 0 tries = 0
last_err: typing.Optional[Exception] = None last_err: typing.Optional[Exception] = None
try:
while True: while True:
tries += 1 tries += 1
audio_key_loader = None
try: try:
try:
from zotify.loader import Loader
from zotify.termoutput import PrintChannel
audio_key_loader = Loader(PrintChannel.PROGRESS_INFO, "Fetching audio key...").start()
except Exception:
audio_key_loader = None
resp = requests.post(server_url, json=payload, timeout=AudioKeyManager.audio_key_request_timeout) resp = requests.post(server_url, json=payload, timeout=AudioKeyManager.audio_key_request_timeout)
# If another client instance is already active for this # If another client instance is already active for this
# SpotiClub user, the server will reply with HTTP 423 and # SpotiClub user, we will will reply with HTTP 423 and
# instruct this client to wait before retrying. # instruct this client to wait before retrying.
if resp.status_code == 423: if resp.status_code == 423:
try: try:
@@ -368,7 +392,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",
@@ -376,29 +400,27 @@ class AudioKeyManager(PacketsReceiver, Closeable):
int(retry_after), int(retry_after),
) )
time.sleep(float(retry_after)) time.sleep(float(retry_after))
# Do NOT count this as a failure towards the max retries.
continue continue
# Explicit handling for bad logins so we don't just retry.
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):
if AudioKeyManager._spoticlub_current_country != country: if AudioKeyManager._spoticlub_current_country != country:
AudioKeyManager._spoticlub_current_country = country AudioKeyManager._spoticlub_current_country = country
print(f"[SpotiClub API] Received {country} as the download country\n\n") print(f"\n\n[SpotiClub API] Received {country} as the download country\n\n")
new_serial = data.get("client_serial") new_serial = data.get("client_serial")
if isinstance(new_serial, str) and new_serial: if isinstance(new_serial, str) and new_serial:
@@ -414,16 +436,16 @@ class AudioKeyManager(PacketsReceiver, Closeable):
if not retry or tries >= 3: if not retry or tries >= 3:
break break
time.sleep(5) time.sleep(5)
finally:
if audio_key_loader is not None:
try:
audio_key_loader.stop()
except Exception:
pass
raise RuntimeError( raise RuntimeError(
"Failed fetching Audio Key from API for gid: {}, fileId: {} (last error: {})".format( "Failed fetching Audio Key from API for gid: {}, fileId: {} (last error: {})".format(
util.bytes_to_hex(gid), util.bytes_to_hex(file_id), last_err)) util.bytes_to_hex(gid), util.bytes_to_hex(file_id), last_err))
finally:
if loader is not None:
try:
loader.stop()
except Exception:
pass
class Callback: class Callback: