rm test.conf

This commit is contained in:
2025-12-18 07:30:39 -05:00
parent 041de95698
commit bc8b407a91
12 changed files with 348 additions and 303 deletions

View File

@@ -15,11 +15,11 @@ import time
# Monkey-patch streamrip's Tidal client credentials BEFORE importing TidalClient
import streamrip.client.tidal as _tidal_module # type: ignore # noqa: E402
_tidal_module.CLIENT_ID = "fX2JxdmntZWK0ixT"
_tidal_module.CLIENT_SECRET = "1Nn9AfDAjxrgJFJbKNWLeAyKGVGmINuXPPLHVXAvxAg="
_tidal_module.AUTH = aiohttp.BasicAuth(
login=_tidal_module.CLIENT_ID,
password=_tidal_module.CLIENT_SECRET
login=_tidal_module.CLIENT_ID, password=_tidal_module.CLIENT_SECRET
)
from streamrip.client import TidalClient # type: ignore # noqa: E402
@@ -99,21 +99,21 @@ class SRUtil:
async def start_keepalive(self) -> None:
"""Start the background keepalive task.
This should be called once at startup to ensure the Tidal session
stays alive even during idle periods.
"""
if self._keepalive_task and not self._keepalive_task.done():
logging.info("Tidal keepalive task already running")
return
# Ensure initial login
try:
await self._login_and_persist()
logging.info("Initial Tidal login successful")
except Exception as e:
logging.warning("Initial Tidal login failed: %s", e)
self._keepalive_task = asyncio.create_task(self._keepalive_runner())
logging.info("Tidal keepalive task started")
@@ -132,14 +132,14 @@ class SRUtil:
while True:
try:
await asyncio.sleep(self.KEEPALIVE_INTERVAL)
# Check if we've had recent activity
if self._last_successful_request:
time_since_last = time.time() - self._last_successful_request
if time_since_last < self.KEEPALIVE_INTERVAL:
# Recent activity, no need to ping
continue
# Check if token is expiring soon and proactively refresh
if self._is_token_expiring_soon():
logging.info("Tidal keepalive: Token expiring soon, refreshing...")
@@ -149,7 +149,7 @@ class SRUtil:
except Exception as e:
logging.warning("Tidal keepalive: Token refresh failed: %s", e)
continue
# Check if session is stale
if self._is_session_stale():
logging.info("Tidal keepalive: Session stale, refreshing...")
@@ -157,9 +157,11 @@ class SRUtil:
await self._login_and_persist(force=True)
logging.info("Tidal keepalive: Session refresh successful")
except Exception as e:
logging.warning("Tidal keepalive: Session refresh failed: %s", e)
logging.warning(
"Tidal keepalive: Session refresh failed: %s", e
)
continue
# Make a lightweight API call to keep the session alive
if self.streamrip_client.logged_in:
try:
@@ -178,7 +180,7 @@ class SRUtil:
await self._login_and_persist(force=True)
except Exception:
pass
except asyncio.CancelledError:
logging.info("Tidal keepalive task cancelled")
break
@@ -195,7 +197,9 @@ class SRUtil:
tidal.access_token = cached.get("access_token", "")
tidal.refresh_token = cached.get("refresh_token", "")
tidal.token_expiry = cached.get("token_expiry", "")
tidal.country_code = cached.get("country_code", os.getenv("tidal_country_code", ""))
tidal.country_code = cached.get(
"country_code", os.getenv("tidal_country_code", "")
)
else:
tidal.user_id = os.getenv("tidal_user_id", "")
tidal.access_token = os.getenv("tidal_access_token", "")
@@ -212,7 +216,9 @@ class SRUtil:
with open(TIDAL_TOKEN_CACHE_PATH, "r") as f:
data = json.load(f)
# Validate required fields exist
if all(k in data for k in ("access_token", "refresh_token", "token_expiry")):
if all(
k in data for k in ("access_token", "refresh_token", "token_expiry")
):
logging.info("Loaded Tidal tokens from cache")
return data
except Exception as e:
@@ -248,22 +254,25 @@ class SRUtil:
async def start_device_auth(self) -> tuple[str, str]:
"""Start device authorization flow.
Returns:
tuple: (device_code, verification_url) - User should visit the URL to authorize.
"""
if not hasattr(self.streamrip_client, 'session') or not self.streamrip_client.session:
if (
not hasattr(self.streamrip_client, "session")
or not self.streamrip_client.session
):
self.streamrip_client.session = await self.streamrip_client.get_session()
device_code, verification_url = await self.streamrip_client._get_device_code()
return device_code, verification_url
async def check_device_auth(self, device_code: str) -> tuple[bool, Optional[str]]:
"""Check if user has completed device authorization.
Args:
device_code: The device code from start_device_auth()
Returns:
tuple: (success, error_message)
- (True, None) if auth completed successfully
@@ -271,7 +280,7 @@ class SRUtil:
- (False, error_message) if auth failed
"""
status, auth_info = await self.streamrip_client._get_auth_status(device_code)
if status == 0:
# Success - apply new tokens
self._apply_new_tokens(auth_info)
@@ -300,7 +309,8 @@ class SRUtil:
# token_expiry is typically an ISO timestamp string
if isinstance(token_expiry, str):
from datetime import datetime
expiry_dt = datetime.fromisoformat(token_expiry.replace('Z', '+00:00'))
expiry_dt = datetime.fromisoformat(token_expiry.replace("Z", "+00:00"))
expiry_ts = expiry_dt.timestamp()
else:
expiry_ts = float(token_expiry)
@@ -318,14 +328,14 @@ class SRUtil:
async def _force_fresh_login(self) -> bool:
"""Force a complete fresh login, ignoring logged_in state.
Returns True if login succeeded, False otherwise.
"""
# Reset the logged_in flag to force a fresh login
self.streamrip_client.logged_in = False
# Close existing session if present
if hasattr(self.streamrip_client, 'session') and self.streamrip_client.session:
if hasattr(self.streamrip_client, "session") and self.streamrip_client.session:
try:
if not self.streamrip_client.session.closed:
await self.streamrip_client.session.close()
@@ -333,10 +343,10 @@ class SRUtil:
logging.warning("Error closing old session: %s", e)
# Use object.__setattr__ to bypass type checking for session reset
try:
object.__setattr__(self.streamrip_client, 'session', None)
object.__setattr__(self.streamrip_client, "session", None)
except Exception:
pass # Session will be recreated on next login
try:
logging.info("Forcing fresh Tidal login...")
await self.streamrip_client.login()
@@ -345,49 +355,53 @@ class SRUtil:
logging.info("Fresh Tidal login successful")
return True
except Exception as e:
logging.warning("Forced Tidal login failed: %s - device re-auth may be required", e)
logging.warning(
"Forced Tidal login failed: %s - device re-auth may be required", e
)
return False
async def _login_and_persist(self, force: bool = False) -> None:
"""Login to Tidal and persist any refreshed tokens.
Args:
force: If True, force a fresh login even if already logged in.
This method now checks for:
1. Token expiry - refreshes if token is about to expire
2. Session age - refreshes if session is too old
3. logged_in state - logs in if not logged in
If refresh fails, logs a warning but does not raise.
"""
needs_login = force or not self.streamrip_client.logged_in
# Check if token is expiring soon
if not needs_login and self._is_token_expiring_soon():
logging.info("Tidal token expiring soon, will refresh")
needs_login = True
# Check if session is too old
if not needs_login and self._is_session_stale():
logging.info("Tidal session is stale, will refresh")
needs_login = True
if not needs_login:
return
try:
# Reset logged_in to ensure fresh login attempt
if force or self._is_token_expiring_soon():
self.streamrip_client.logged_in = False
await self.streamrip_client.login()
self._last_login_time = time.time()
# After login, tokens may have been refreshed - persist them
self._save_cached_tokens()
logging.info("Tidal login/refresh successful")
except Exception as e:
logging.warning("Tidal login/refresh failed: %s - device re-auth may be required", e)
logging.warning(
"Tidal login/refresh failed: %s - device re-auth may be required", e
)
# Don't mark as logged in on failure - let subsequent calls retry
async def rate_limited_request(self, func, *args, **kwargs):
@@ -397,13 +411,15 @@ class SRUtil:
elapsed = now - self.LAST_METADATA_REQUEST
if elapsed < self.METADATA_RATE_LIMIT:
await asyncio.sleep(self.METADATA_RATE_LIMIT - elapsed)
# Ensure we're logged in before making the request
try:
await self._login_and_persist()
except Exception as e:
logging.warning("Pre-request login failed in rate_limited_request: %s", e)
logging.warning(
"Pre-request login failed in rate_limited_request: %s", e
)
result = await func(*args, **kwargs)
self.LAST_METADATA_REQUEST = time.time()
return result
@@ -432,8 +448,11 @@ class SRUtil:
try:
await self._login_and_persist()
except Exception as login_err:
logging.warning("Pre-request login failed: %s (continuing anyway)", login_err)
logging.warning(
"Pre-request login failed: %s (continuing anyway)",
login_err,
)
result = await func(*args, **kwargs)
# Track successful request
self._last_successful_request = time.time()
@@ -441,7 +460,12 @@ class SRUtil:
except AttributeError as e:
# Probably missing/closed client internals: try re-login once
last_exc = e
logging.warning("AttributeError in API call (attempt %d/%d): %s", attempt + 1, retries, e)
logging.warning(
"AttributeError in API call (attempt %d/%d): %s",
attempt + 1,
retries,
e,
)
try:
await self._force_fresh_login()
except Exception:
@@ -475,7 +499,10 @@ class SRUtil:
# Treat 401 (Unauthorized) as an auth failure: force a fresh re-login then retry
is_401_error = (
(isinstance(e, aiohttp.ClientResponseError) and getattr(e, "status", None) == 401)
(
isinstance(e, aiohttp.ClientResponseError)
and getattr(e, "status", None) == 401
)
or "401" in msg
or "unauthorized" in msg.lower()
)
@@ -491,7 +518,9 @@ class SRUtil:
if login_success:
logging.info("Forced re-login after 401 successful")
else:
logging.warning("Forced re-login after 401 failed - may need device re-auth")
logging.warning(
"Forced re-login after 401 failed - may need device re-auth"
)
except Exception as login_exc:
logging.warning("Forced login after 401 failed: %s", login_exc)
if attempt < retries - 1:
@@ -550,9 +579,7 @@ class SRUtil:
title_match = self.is_fuzzy_match(expected_title, found_title, threshold)
return artist_match and album_match and title_match
def dedupe_by_key(
self, key: str | list[str], entries: list[dict]
) -> list[dict]:
def dedupe_by_key(self, key: str | list[str], entries: list[dict]) -> list[dict]:
"""Return entries de-duplicated by one or more keys."""
keys = [key] if isinstance(key, str) else list(key)
@@ -679,9 +706,11 @@ class SRUtil:
"upc": album_json.get("upc"),
"album_copyright": album_json.get("copyright"),
"album_cover_id": album_json.get("cover"),
"album_cover_url": f"https://resources.tidal.com/images/{album_json.get('cover')}/1280x1280.jpg"
if album_json.get("cover")
else None,
"album_cover_url": (
f"https://resources.tidal.com/images/{album_json.get('cover')}/1280x1280.jpg"
if album_json.get("cover")
else None
),
}
# Track-level (overrides or adds to album info)
@@ -813,7 +842,9 @@ class SRUtil:
return None
if not metadata:
return None
albums = self.dedupe_by_key(["title", "releaseDate"], metadata.get("albums", []))
albums = self.dedupe_by_key(
["title", "releaseDate"], metadata.get("albums", [])
)
albums_out = [
{
"artist": ", ".join(artist["name"] for artist in album["artists"]),