formatting / minor

This commit is contained in:
2025-11-22 21:43:48 -05:00
parent 3d0b867427
commit 353f14c899
7 changed files with 178 additions and 108 deletions

View File

@@ -25,7 +25,7 @@ from typing import Optional
logging.basicConfig( logging.basicConfig(
filename="cync_auth_events.log", filename="cync_auth_events.log",
level=logging.INFO, level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s" format="%(asctime)s - %(levelname)s - %(message)s",
) )
@@ -35,6 +35,7 @@ def _mask_token(token: Optional[str]) -> str:
return "<invalid_token>" return "<invalid_token>"
return f"{token[:4]}...{token[-4:]}" return f"{token[:4]}...{token[-4:]}"
def _log_token_state(user, context: str): def _log_token_state(user, context: str):
"""Log masked token state for debugging.""" """Log masked token state for debugging."""
if not user: if not user:
@@ -44,15 +45,14 @@ def _log_token_state(user, context: str):
try: try:
logging.info( logging.info(
f"{context} - Token state: access=%s refresh=%s expires_at=%s", f"{context} - Token state: access=%s refresh=%s expires_at=%s",
_mask_token(getattr(user, 'access_token', None)), _mask_token(getattr(user, "access_token", None)),
_mask_token(getattr(user, 'refresh_token', None)), _mask_token(getattr(user, "refresh_token", None)),
getattr(user, 'expires_at', None) getattr(user, "expires_at", None),
) )
except Exception as e: except Exception as e:
logging.error(f"Error logging token state: {e}") logging.error(f"Error logging token state: {e}")
class Lighting(FastAPI): class Lighting(FastAPI):
async def _close_session_safely(self): async def _close_session_safely(self):
"""Safely close the current session if it exists.""" """Safely close the current session if it exists."""
@@ -69,7 +69,11 @@ class Lighting(FastAPI):
async def _test_connection_health(self) -> bool: async def _test_connection_health(self) -> bool:
"""Test if the current connection is healthy by making a simple API call.""" """Test if the current connection is healthy by making a simple API call."""
if not self.cync_api or not self.session or getattr(self.session, "closed", True): if (
not self.cync_api
or not self.session
or getattr(self.session, "closed", True)
):
return False return False
try: try:
@@ -102,7 +106,9 @@ class Lighting(FastAPI):
# If force_reconnect is True or connection is unhealthy, rebuild everything # If force_reconnect is True or connection is unhealthy, rebuild everything
if force_reconnect or not await self._test_connection_health(): if force_reconnect or not await self._test_connection_health():
logging.info("Connection unhealthy or force reconnect requested. Rebuilding connection...") logging.info(
"Connection unhealthy or force reconnect requested. Rebuilding connection..."
)
# Clean up existing connection # Clean up existing connection
await self._close_session_safely() await self._close_session_safely()
@@ -115,12 +121,9 @@ class Lighting(FastAPI):
ttl_dns_cache=300, ttl_dns_cache=300,
use_dns_cache=True, use_dns_cache=True,
keepalive_timeout=60, keepalive_timeout=60,
enable_cleanup_closed=True enable_cleanup_closed=True,
)
self.session = aiohttp.ClientSession(
timeout=timeout,
connector=connector
) )
self.session = aiohttp.ClientSession(timeout=timeout, connector=connector)
# Load cached token and check validity # Load cached token and check validity
self.cync_user = None self.cync_user = None
@@ -178,12 +181,18 @@ class Lighting(FastAPI):
if twofa_code: if twofa_code:
logging.info("Retrying Cync login with 2FA code.") logging.info("Retrying Cync login with 2FA code.")
try: try:
self.cync_user = await self.auth.login(two_factor_code=twofa_code) self.cync_user = await self.auth.login(
two_factor_code=twofa_code
)
self._save_cached_user(self.cync_user) self._save_cached_user(self.cync_user)
logging.info("Logged in with 2FA successfully.") logging.info("Logged in with 2FA successfully.")
except Exception as e: except Exception as e:
logging.error("Cync 2FA login failed: %s", e) logging.error("Cync 2FA login failed: %s", e)
logging.info("2FA failure details: Code=%s, User=%s", twofa_code, self.cync_user) logging.info(
"2FA failure details: Code=%s, User=%s",
twofa_code,
self.cync_user,
)
raise Exception("Cync 2FA code invalid or not accepted.") raise Exception("Cync 2FA code invalid or not accepted.")
else: else:
logging.error("Cync 2FA required but no code provided.") logging.error("Cync 2FA required but no code provided.")
@@ -210,27 +219,47 @@ class Lighting(FastAPI):
"error_message": str(e), "error_message": str(e),
"auth_state": { "auth_state": {
"has_auth": bool(self.auth), "has_auth": bool(self.auth),
"has_user": bool(getattr(self.auth, 'user', None)), "has_user": bool(getattr(self.auth, "user", None)),
"user_state": { "user_state": {
"access_token": _mask_token(getattr(self.auth.user, 'access_token', None)) if self.auth and self.auth.user else None, "access_token": _mask_token(
"refresh_token": _mask_token(getattr(self.auth.user, 'refresh_token', None)) if self.auth and self.auth.user else None, getattr(self.auth.user, "access_token", None)
"expires_at": getattr(self.auth.user, 'expires_at', None) if self.auth and self.auth.user else None )
} if self.auth and self.auth.user else None if self.auth and self.auth.user
else None,
"refresh_token": _mask_token(
getattr(self.auth.user, "refresh_token", None)
)
if self.auth and self.auth.user
else None,
"expires_at": getattr(self.auth.user, "expires_at", None)
if self.auth and self.auth.user
else None,
} }
if self.auth and self.auth.user
else None,
},
} }
diagnostic_file = f"cync_api_failure-{int(time.time())}.json" diagnostic_file = f"cync_api_failure-{int(time.time())}.json"
try: try:
with open(diagnostic_file, 'w') as f: with open(diagnostic_file, "w") as f:
json.dump(diagnostic_data, f, indent=2) json.dump(diagnostic_data, f, indent=2)
logging.info(f"Saved API creation diagnostic data to {diagnostic_file}") logging.info(
f"Saved API creation diagnostic data to {diagnostic_file}"
)
except Exception as save_error: except Exception as save_error:
logging.error(f"Failed to save diagnostic data: {save_error}") logging.error(f"Failed to save diagnostic data: {save_error}")
raise raise
# Final validation # Final validation
if not self.cync_api or not self.session or getattr(self.session, "closed", True): if (
not self.cync_api
or not self.session
or getattr(self.session, "closed", True)
):
logging.error("Connection validation failed after setup") logging.error("Connection validation failed after setup")
_log_token_state(getattr(self.auth, 'user', None), "Failed connection validation") _log_token_state(
getattr(self.auth, "user", None), "Failed connection validation"
)
raise Exception("Failed to establish proper Cync connection") raise Exception("Failed to establish proper Cync connection")
""" """
@@ -334,18 +363,20 @@ class Lighting(FastAPI):
logging.error("Auth object is not initialized.") logging.error("Auth object is not initialized.")
raise Exception("Cync authentication not initialized.") raise Exception("Cync authentication not initialized.")
try: try:
user = getattr(self.auth, 'user', None) user = getattr(self.auth, "user", None)
_log_token_state(user, "Before refresh attempt") _log_token_state(user, "Before refresh attempt")
if user and hasattr(user, "expires_at") and user.expires_at > time.time(): if user and hasattr(user, "expires_at") and user.expires_at > time.time():
refresh = getattr(self.auth, 'async_refresh_user_token', None) refresh = getattr(self.auth, "async_refresh_user_token", None)
if callable(refresh): if callable(refresh):
try: try:
logging.info("Attempting token refresh...") logging.info("Attempting token refresh...")
result = refresh() result = refresh()
if inspect.isawaitable(result): if inspect.isawaitable(result):
await result await result
logging.info("Token refresh completed successfully (awaited)") logging.info(
"Token refresh completed successfully (awaited)"
)
else: else:
logging.info("Token refresh completed (non-awaitable)") logging.info("Token refresh completed (non-awaitable)")
except AuthFailedError as e: except AuthFailedError as e:
@@ -359,20 +390,28 @@ class Lighting(FastAPI):
"error_type": "AuthFailedError", "error_type": "AuthFailedError",
"error_message": str(e), "error_message": str(e),
"user_state": { "user_state": {
"access_token": _mask_token(getattr(user, 'access_token', None)), "access_token": _mask_token(
"refresh_token": _mask_token(getattr(user, 'refresh_token', None)), getattr(user, "access_token", None)
"expires_at": getattr(user, 'expires_at', None) ),
} "refresh_token": _mask_token(
getattr(user, "refresh_token", None)
),
"expires_at": getattr(user, "expires_at", None),
},
} }
try: try:
diagnostic_file = f"cync_auth_failure-{int(time.time())}.json" diagnostic_file = (
with open(diagnostic_file, 'w') as f: f"cync_auth_failure-{int(time.time())}.json"
)
with open(diagnostic_file, "w") as f:
json.dump(diagnostic_data, f, indent=2) json.dump(diagnostic_data, f, indent=2)
logging.info(f"Saved diagnostic data to {diagnostic_file}") logging.info(f"Saved diagnostic data to {diagnostic_file}")
except Exception as save_error: except Exception as save_error:
logging.error(f"Failed to save diagnostic data: {save_error}") logging.error(
f"Failed to save diagnostic data: {save_error}"
)
raise raise
login = getattr(self.auth, 'login', None) login = getattr(self.auth, "login", None)
if callable(login): if callable(login):
try: try:
result = login() result = login()
@@ -400,7 +439,11 @@ class Lighting(FastAPI):
logging.info("Logged in with 2FA successfully.") logging.info("Logged in with 2FA successfully.")
except Exception as e: except Exception as e:
logging.error("Cync 2FA login failed: %s", e) logging.error("Cync 2FA login failed: %s", e)
logging.info("2FA failure details: Code=%s, User=%s", twofa_code, self.cync_user) logging.info(
"2FA failure details: Code=%s, User=%s",
twofa_code,
self.cync_user,
)
raise Exception("Cync 2FA code invalid or not accepted.") raise Exception("Cync 2FA code invalid or not accepted.")
else: else:
logging.error("Cync 2FA required but no code provided.") logging.error("Cync 2FA required but no code provided.")
@@ -425,15 +468,21 @@ class Lighting(FastAPI):
expires_at = getattr(self.cync_user, "expires_at", 0) expires_at = getattr(self.cync_user, "expires_at", 0)
time_until_expiry = expires_at - time.time() time_until_expiry = expires_at - time.time()
if time_until_expiry < 600: # Less than 10 minutes if time_until_expiry < 600: # Less than 10 minutes
logging.info(f"Token expires in {int(time_until_expiry/60)} minutes. Refreshing...") logging.info(
f"Token expires in {int(time_until_expiry / 60)} minutes. Refreshing..."
)
try: try:
await self._refresh_or_login() await self._refresh_or_login()
except Exception as e: except Exception as e:
logging.error(f"Token refresh failed during health check: {e}") logging.error(
f"Token refresh failed during health check: {e}"
)
# Test connection health # Test connection health
if not await self._test_connection_health(): if not await self._test_connection_health():
logging.warning("Connection health check failed. Will reconnect on next API call.") logging.warning(
"Connection health check failed. Will reconnect on next API call."
)
except asyncio.CancelledError: except asyncio.CancelledError:
logging.info("Health check task cancelled") logging.info("Health check task cancelled")
@@ -558,13 +607,17 @@ class Lighting(FastAPI):
if not self.cync_api: if not self.cync_api:
raise Exception("Cync API not available after connection setup") raise Exception("Cync API not available after connection setup")
logging.info(f"Attempt {attempt + 1}/{max_retries}: Getting devices from Cync API...") logging.info(
f"Attempt {attempt + 1}/{max_retries}: Getting devices from Cync API..."
)
devices = self.cync_api.get_devices() devices = self.cync_api.get_devices()
if not devices: if not devices:
raise Exception("No devices returned from Cync API") raise Exception("No devices returned from Cync API")
logging.info(f"Devices returned: {[getattr(d, 'name', 'unnamed') for d in devices]}") logging.info(
f"Devices returned: {[getattr(d, 'name', 'unnamed') for d in devices]}"
)
light = next( light = next(
( (
@@ -576,10 +629,16 @@ class Lighting(FastAPI):
) )
if not light: if not light:
available_devices = [getattr(d, 'name', 'unnamed') for d in devices] available_devices = [
raise Exception(f"Device '{self.cync_device_name}' not found. Available devices: {available_devices}") getattr(d, "name", "unnamed") for d in devices
]
raise Exception(
f"Device '{self.cync_device_name}' not found. Available devices: {available_devices}"
)
logging.info(f"Selected device: {getattr(light, 'name', 'unnamed')}") logging.info(
f"Selected device: {getattr(light, 'name', 'unnamed')}"
)
# Execute device operations # Execute device operations
operations_completed = [] operations_completed = []
@@ -595,14 +654,18 @@ class Lighting(FastAPI):
# Set brightness # Set brightness
if "brightness" in state: if "brightness" in state:
result = await light.set_brightness(brightness) result = await light.set_brightness(brightness)
operations_completed.append(f"set_brightness({brightness}): {result}") operations_completed.append(
f"set_brightness({brightness}): {result}"
)
# Set color # Set color
if rgb: if rgb:
result = await light.set_rgb(rgb) result = await light.set_rgb(rgb)
operations_completed.append(f"set_rgb({rgb}): {result}") operations_completed.append(f"set_rgb({rgb}): {result}")
logging.info(f"All operations completed successfully: {operations_completed}") logging.info(
f"All operations completed successfully: {operations_completed}"
)
break # Success, exit retry loop break # Success, exit retry loop
except ( except (
@@ -613,7 +676,7 @@ class Lighting(FastAPI):
ConnectionResetError, ConnectionResetError,
ConnectionError, ConnectionError,
OSError, OSError,
asyncio.TimeoutError asyncio.TimeoutError,
) as e: ) as e:
last_exception = e last_exception = e
logging.warning( logging.warning(
@@ -621,12 +684,16 @@ class Lighting(FastAPI):
) )
if attempt < max_retries - 1: if attempt < max_retries - 1:
# Wait a bit before retry to allow network/server recovery # Wait a bit before retry to allow network/server recovery
await asyncio.sleep(2 ** attempt) # Exponential backoff: 1s, 2s, 4s await asyncio.sleep(
2**attempt
) # Exponential backoff: 1s, 2s, 4s
continue continue
except (AuthFailedError, TwoFactorRequiredError) as e: except (AuthFailedError, TwoFactorRequiredError) as e:
last_exception = e last_exception = e
logging.error(f"Authentication error (attempt {attempt + 1}/{max_retries}): {e}") logging.error(
f"Authentication error (attempt {attempt + 1}/{max_retries}): {e}"
)
if attempt < max_retries - 1: if attempt < max_retries - 1:
# Clear cached tokens on auth errors # Clear cached tokens on auth errors
try: try:
@@ -644,12 +711,16 @@ class Lighting(FastAPI):
# On unexpected errors, try reconnecting for next attempt # On unexpected errors, try reconnecting for next attempt
if attempt < max_retries - 1: if attempt < max_retries - 1:
logging.warning("Forcing full reconnection due to unexpected error...") logging.warning(
"Forcing full reconnection due to unexpected error..."
)
await asyncio.sleep(1) await asyncio.sleep(1)
continue continue
# If we get here, all retries failed # If we get here, all retries failed
logging.error(f"All {max_retries} attempts failed. Last error: {type(last_exception).__name__}: {last_exception}") logging.error(
f"All {max_retries} attempts failed. Last error: {type(last_exception).__name__}: {last_exception}"
)
raise last_exception raise last_exception
logging.info( logging.info(

View File

@@ -766,7 +766,7 @@ class Radio(FastAPI):
# Try SR first with timeout # Try SR first with timeout
try: try:
async with asyncio.timeout(10.0): # 5 second timeout async with asyncio.timeout(10.0): # 10 second timeout
lrc = await self.sr_util.get_lrc_by_artist_song( lrc = await self.sr_util.get_lrc_by_artist_song(
artist, title, duration=duration artist, title, duration=duration
) )

View File

@@ -23,6 +23,7 @@ from pydantic import BaseModel
logger = logging.getLogger() logger = logging.getLogger()
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
class ValidBulkFetchRequest(BaseModel): class ValidBulkFetchRequest(BaseModel):
track_ids: list[int] track_ids: list[int]
target: str target: str

View File

@@ -1,6 +1,7 @@
""" """
Database models for LRCLib lyrics cache. Database models for LRCLib lyrics cache.
""" """
import os import os
import urllib.parse import urllib.parse
from typing import Type, AsyncGenerator from typing import Type, AsyncGenerator
@@ -24,6 +25,7 @@ Base: Type[DeclarativeMeta] = declarative_base()
class Tracks(Base): # type: ignore class Tracks(Base): # type: ignore
"""Tracks table - stores track metadata.""" """Tracks table - stores track metadata."""
__tablename__ = "tracks" __tablename__ = "tracks"
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
@@ -60,6 +62,7 @@ class Tracks(Base): # type: ignore
class Lyrics(Base): # type: ignore class Lyrics(Base): # type: ignore
"""Lyrics table - stores lyrics content.""" """Lyrics table - stores lyrics content."""
__tablename__ = "lyrics" __tablename__ = "lyrics"
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
@@ -95,11 +98,7 @@ encoded_password = urllib.parse.quote_plus(POSTGRES_PASSWORD)
DATABASE_URL: str = f"postgresql+asyncpg://{POSTGRES_USER}:{encoded_password}@{POSTGRES_HOST}:{POSTGRES_PORT}/{POSTGRES_DB}" DATABASE_URL: str = f"postgresql+asyncpg://{POSTGRES_USER}:{encoded_password}@{POSTGRES_HOST}:{POSTGRES_PORT}/{POSTGRES_DB}"
async_engine: AsyncEngine = create_async_engine( async_engine: AsyncEngine = create_async_engine(
DATABASE_URL, DATABASE_URL, pool_size=20, max_overflow=10, pool_pre_ping=True, echo=False
pool_size=20,
max_overflow=10,
pool_pre_ping=True,
echo=False
) )
AsyncSessionLocal = async_sessionmaker(bind=async_engine, expire_on_commit=False) AsyncSessionLocal = async_sessionmaker(bind=async_engine, expire_on_commit=False)

View File

@@ -125,8 +125,7 @@ class LRCLib:
input_track = f"{artist} - {song}" input_track = f"{artist} - {song}"
returned_track = f"{returned_artist} - {returned_song}" returned_track = f"{returned_artist} - {returned_song}"
match_result = self.matcher.find_best_match( match_result = self.matcher.find_best_match(
input_track=input_track, input_track=input_track, candidate_tracks=[(0, returned_track)]
candidate_tracks=[(0, returned_track)]
) )
if not match_result: if not match_result:

View File

@@ -795,7 +795,7 @@ class SRUtil:
tracks_with_diff.sort(key=lambda x: x[1]) tracks_with_diff.sort(key=lambda x: x[1])
best_track, min_diff = tracks_with_diff[0] best_track, min_diff = tracks_with_diff[0]
logging.info(f"SR: Best match duration diff: {min_diff}s") logging.info(f"SR: Best match duration diff: {min_diff}s")
# If the closest match is more than 5 seconds off, consider no match # If the closest match is more than 10 seconds off, consider no match
if min_diff > 10: if min_diff > 10:
logging.info("SR: Duration diff too large, no match") logging.info("SR: Duration diff too large, no match")
return None return None