This commit is contained in:
@@ -400,7 +400,6 @@ async def play_sound_with_vlc(
|
|||||||
await credit_service.validate_and_reserve_credits(
|
await credit_service.validate_and_reserve_credits(
|
||||||
current_user.id,
|
current_user.id,
|
||||||
CreditActionType.VLC_PLAY_SOUND,
|
CreditActionType.VLC_PLAY_SOUND,
|
||||||
{"sound_id": sound_id, "sound_name": sound.name},
|
|
||||||
)
|
)
|
||||||
except InsufficientCreditsError as e:
|
except InsufficientCreditsError as e:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -418,8 +417,8 @@ async def play_sound_with_vlc(
|
|||||||
await credit_service.deduct_credits(
|
await credit_service.deduct_credits(
|
||||||
current_user.id,
|
current_user.id,
|
||||||
CreditActionType.VLC_PLAY_SOUND,
|
CreditActionType.VLC_PLAY_SOUND,
|
||||||
success,
|
success=success,
|
||||||
{"sound_id": sound_id, "sound_name": sound.name},
|
metadata={"sound_id": sound_id, "sound_name": sound.name},
|
||||||
)
|
)
|
||||||
|
|
||||||
if not success:
|
if not success:
|
||||||
|
|||||||
@@ -76,14 +76,12 @@ class CreditService:
|
|||||||
self,
|
self,
|
||||||
user_id: int,
|
user_id: int,
|
||||||
action_type: CreditActionType,
|
action_type: CreditActionType,
|
||||||
metadata: dict[str, Any] | None = None,
|
|
||||||
) -> tuple[User, CreditAction]:
|
) -> tuple[User, CreditAction]:
|
||||||
"""Validate user has sufficient credits and optionally reserve them.
|
"""Validate user has sufficient credits and optionally reserve them.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user_id: The user ID
|
user_id: The user ID
|
||||||
action_type: The type of action
|
action_type: The type of action
|
||||||
metadata: Optional metadata to store with transaction
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple of (user, credit_action)
|
Tuple of (user, credit_action)
|
||||||
@@ -118,6 +116,7 @@ class CreditService:
|
|||||||
self,
|
self,
|
||||||
user_id: int,
|
user_id: int,
|
||||||
action_type: CreditActionType,
|
action_type: CreditActionType,
|
||||||
|
*,
|
||||||
success: bool = True,
|
success: bool = True,
|
||||||
metadata: dict[str, Any] | None = None,
|
metadata: dict[str, Any] | None = None,
|
||||||
) -> CreditTransaction:
|
) -> CreditTransaction:
|
||||||
@@ -139,18 +138,26 @@ class CreditService:
|
|||||||
"""
|
"""
|
||||||
action = get_credit_action(action_type)
|
action = get_credit_action(action_type)
|
||||||
|
|
||||||
# Only deduct if action requires success and was successful, or doesn't require success
|
# Only deduct if action requires success and was successful,
|
||||||
should_deduct = (action.requires_success and success) or not action.requires_success
|
# or doesn't require success
|
||||||
|
should_deduct = (
|
||||||
|
action.requires_success and success
|
||||||
|
) or not action.requires_success
|
||||||
|
|
||||||
if not should_deduct:
|
if not should_deduct:
|
||||||
logger.info(
|
logger.info(
|
||||||
"Skipping credit deduction for user %s: action %s failed and requires success",
|
"Skipping credit deduction for user %s: "
|
||||||
|
"action %s failed and requires success",
|
||||||
user_id,
|
user_id,
|
||||||
action_type.value,
|
action_type.value,
|
||||||
)
|
)
|
||||||
# Still create a transaction record for auditing
|
# Still create a transaction record for auditing
|
||||||
return await self._create_transaction_record(
|
return await self._create_transaction_record(
|
||||||
user_id, action, 0, success, metadata,
|
user_id,
|
||||||
|
action,
|
||||||
|
0,
|
||||||
|
success=success,
|
||||||
|
metadata=metadata,
|
||||||
)
|
)
|
||||||
|
|
||||||
session = self.db_session_factory()
|
session = self.db_session_factory()
|
||||||
@@ -204,13 +211,18 @@ class CreditService:
|
|||||||
"action_type": action_type.value,
|
"action_type": action_type.value,
|
||||||
"success": success,
|
"success": success,
|
||||||
}
|
}
|
||||||
await socket_manager.send_to_user(str(user_id), "user_credits_changed", event_data)
|
await socket_manager.send_to_user(
|
||||||
|
str(user_id),
|
||||||
|
"user_credits_changed",
|
||||||
|
event_data,
|
||||||
|
)
|
||||||
logger.info("Emitted user_credits_changed event for user %s", user_id)
|
logger.info("Emitted user_credits_changed event for user %s", user_id)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception(
|
logger.exception(
|
||||||
"Failed to emit user_credits_changed event for user %s", user_id,
|
"Failed to emit user_credits_changed event for user %s",
|
||||||
|
user_id,
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
return transaction
|
return transaction
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -292,13 +304,18 @@ class CreditService:
|
|||||||
"description": description,
|
"description": description,
|
||||||
"success": True,
|
"success": True,
|
||||||
}
|
}
|
||||||
await socket_manager.send_to_user(str(user_id), "user_credits_changed", event_data)
|
await socket_manager.send_to_user(
|
||||||
|
str(user_id),
|
||||||
|
"user_credits_changed",
|
||||||
|
event_data,
|
||||||
|
)
|
||||||
logger.info("Emitted user_credits_changed event for user %s", user_id)
|
logger.info("Emitted user_credits_changed event for user %s", user_id)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception(
|
logger.exception(
|
||||||
"Failed to emit user_credits_changed event for user %s", user_id,
|
"Failed to emit user_credits_changed event for user %s",
|
||||||
|
user_id,
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
return transaction
|
return transaction
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -312,6 +329,7 @@ class CreditService:
|
|||||||
user_id: int,
|
user_id: int,
|
||||||
action: CreditAction,
|
action: CreditAction,
|
||||||
amount: int,
|
amount: int,
|
||||||
|
*,
|
||||||
success: bool,
|
success: bool,
|
||||||
metadata: dict[str, Any] | None = None,
|
metadata: dict[str, Any] | None = None,
|
||||||
) -> CreditTransaction:
|
) -> CreditTransaction:
|
||||||
@@ -342,19 +360,22 @@ class CreditService:
|
|||||||
amount=amount,
|
amount=amount,
|
||||||
balance_before=user.credits,
|
balance_before=user.credits,
|
||||||
balance_after=user.credits,
|
balance_after=user.credits,
|
||||||
description=f"{action.description} (failed)" if not success else action.description,
|
description=(
|
||||||
|
f"{action.description} (failed)"
|
||||||
|
if not success
|
||||||
|
else action.description
|
||||||
|
),
|
||||||
success=success,
|
success=success,
|
||||||
metadata_json=json.dumps(metadata) if metadata else None,
|
metadata_json=json.dumps(metadata) if metadata else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
session.add(transaction)
|
session.add(transaction)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
return transaction
|
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
await session.rollback()
|
await session.rollback()
|
||||||
raise
|
raise
|
||||||
|
else:
|
||||||
|
return transaction
|
||||||
finally:
|
finally:
|
||||||
await session.close()
|
await session.close()
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,10 @@ class ExtractionService:
|
|||||||
|
|
||||||
extraction = await self.extraction_repo.create(extraction_data)
|
extraction = await self.extraction_repo.create(extraction_data)
|
||||||
logger.info("Created extraction with ID: %d", extraction.id)
|
logger.info("Created extraction with ID: %d", extraction.id)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to create extraction for URL: %s", url)
|
||||||
|
raise
|
||||||
|
else:
|
||||||
return {
|
return {
|
||||||
"id": extraction.id or 0, # Should never be None for created extraction
|
"id": extraction.id or 0, # Should never be None for created extraction
|
||||||
"url": extraction.url,
|
"url": extraction.url,
|
||||||
@@ -87,10 +90,6 @@ class ExtractionService:
|
|||||||
"sound_id": extraction.sound_id,
|
"sound_id": extraction.sound_id,
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception:
|
|
||||||
logger.exception("Failed to create extraction for URL: %s", url)
|
|
||||||
raise
|
|
||||||
|
|
||||||
async def _detect_service_info(self, url: str) -> dict[str, str | None] | None:
|
async def _detect_service_info(self, url: str) -> dict[str, str | None] | None:
|
||||||
"""Detect service information from URL using yt-dlp."""
|
"""Detect service information from URL using yt-dlp."""
|
||||||
try:
|
try:
|
||||||
@@ -126,10 +125,12 @@ class ExtractionService:
|
|||||||
"""Process an extraction job."""
|
"""Process an extraction job."""
|
||||||
extraction = await self.extraction_repo.get_by_id(extraction_id)
|
extraction = await self.extraction_repo.get_by_id(extraction_id)
|
||||||
if not extraction:
|
if not extraction:
|
||||||
raise ValueError(f"Extraction {extraction_id} not found")
|
msg = f"Extraction {extraction_id} not found"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
if extraction.status != "pending":
|
if extraction.status != "pending":
|
||||||
raise ValueError(f"Extraction {extraction_id} is not pending")
|
msg = f"Extraction {extraction_id} is not pending"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
# Store all needed values early to avoid session detachment issues
|
# Store all needed values early to avoid session detachment issues
|
||||||
user_id = extraction.user_id
|
user_id = extraction.user_id
|
||||||
@@ -150,7 +151,8 @@ class ExtractionService:
|
|||||||
service_info = await self._detect_service_info(extraction_url)
|
service_info = await self._detect_service_info(extraction_url)
|
||||||
|
|
||||||
if not service_info:
|
if not service_info:
|
||||||
raise ValueError("Unable to detect service information from URL")
|
msg = "Unable to detect service information from URL"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
# Check if extraction already exists for this service
|
# Check if extraction already exists for this service
|
||||||
existing = await self.extraction_repo.get_by_service_and_id(
|
existing = await self.extraction_repo.get_by_service_and_id(
|
||||||
@@ -222,7 +224,12 @@ class ExtractionService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
logger.info("Successfully processed extraction %d", extraction_id)
|
logger.info("Successfully processed extraction %d", extraction_id)
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = str(e)
|
||||||
|
logger.exception(
|
||||||
|
"Failed to process extraction %d: %s", extraction_id, error_msg,
|
||||||
|
)
|
||||||
|
else:
|
||||||
return {
|
return {
|
||||||
"id": extraction_id,
|
"id": extraction_id,
|
||||||
"url": extraction_url,
|
"url": extraction_url,
|
||||||
@@ -234,12 +241,6 @@ class ExtractionService:
|
|||||||
"sound_id": sound_id,
|
"sound_id": sound_id,
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
error_msg = str(e)
|
|
||||||
logger.exception(
|
|
||||||
"Failed to process extraction %d: %s", extraction_id, error_msg,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Update extraction with error
|
# Update extraction with error
|
||||||
await self.extraction_repo.update(
|
await self.extraction_repo.update(
|
||||||
extraction,
|
extraction,
|
||||||
@@ -313,7 +314,8 @@ class ExtractionService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not audio_files:
|
if not audio_files:
|
||||||
raise RuntimeError("No audio file was created during extraction")
|
msg = "No audio file was created during extraction"
|
||||||
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
audio_file = audio_files[0]
|
audio_file = audio_files[0]
|
||||||
thumbnail_file = thumbnail_files[0] if thumbnail_files else None
|
thumbnail_file = thumbnail_files[0] if thumbnail_files else None
|
||||||
@@ -324,11 +326,12 @@ class ExtractionService:
|
|||||||
thumbnail_file or "None",
|
thumbnail_file or "None",
|
||||||
)
|
)
|
||||||
|
|
||||||
return audio_file, thumbnail_file
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("yt-dlp extraction failed for %s", extraction_url)
|
logger.exception("yt-dlp extraction failed for %s", extraction_url)
|
||||||
raise RuntimeError(f"Audio extraction failed: {e}") from e
|
error_msg = f"Audio extraction failed: {e}"
|
||||||
|
raise RuntimeError(error_msg) from e
|
||||||
|
else:
|
||||||
|
return audio_file, thumbnail_file
|
||||||
|
|
||||||
async def _move_files_to_final_location(
|
async def _move_files_to_final_location(
|
||||||
self,
|
self,
|
||||||
@@ -450,8 +453,8 @@ class ExtractionService:
|
|||||||
else:
|
else:
|
||||||
logger.info("Successfully normalized sound %d", sound_id)
|
logger.info("Successfully normalized sound %d", sound_id)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.exception("Error normalizing sound %d: %s", sound_id, e)
|
logger.exception("Error normalizing sound %d", sound_id)
|
||||||
# Don't fail the extraction if normalization fails
|
# Don't fail the extraction if normalization fails
|
||||||
|
|
||||||
async def _add_to_main_playlist(self, sound_id: int, user_id: int) -> None:
|
async def _add_to_main_playlist(self, sound_id: int, user_id: int) -> None:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""Background extraction processor for handling extraction queue."""
|
"""Background extraction processor for handling extraction queue."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import contextlib
|
||||||
|
|
||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
@@ -51,10 +52,8 @@ class ExtractionProcessor:
|
|||||||
"Extraction processor did not stop gracefully, cancelling...",
|
"Extraction processor did not stop gracefully, cancelling...",
|
||||||
)
|
)
|
||||||
self.processor_task.cancel()
|
self.processor_task.cancel()
|
||||||
try:
|
with contextlib.suppress(asyncio.CancelledError):
|
||||||
await self.processor_task
|
await self.processor_task
|
||||||
except asyncio.CancelledError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
logger.info("Extraction processor stopped")
|
logger.info("Extraction processor stopped")
|
||||||
|
|
||||||
@@ -84,8 +83,8 @@ class ExtractionProcessor:
|
|||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
continue # Continue processing
|
continue # Continue processing
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.exception("Error in extraction queue processor: %s", e)
|
logger.exception("Error in extraction queue processor")
|
||||||
# Wait a bit before retrying to avoid tight error loops
|
# Wait a bit before retrying to avoid tight error loops
|
||||||
try:
|
try:
|
||||||
await asyncio.wait_for(self.shutdown_event.wait(), timeout=10.0)
|
await asyncio.wait_for(self.shutdown_event.wait(), timeout=10.0)
|
||||||
@@ -156,8 +155,8 @@ class ExtractionProcessor:
|
|||||||
result["status"],
|
result["status"],
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.exception("Error processing extraction %d: %s", extraction_id, e)
|
logger.exception("Error processing extraction %d", extraction_id)
|
||||||
|
|
||||||
def _on_extraction_completed(self, extraction_id: int, task: asyncio.Task) -> None:
|
def _on_extraction_completed(self, extraction_id: int, task: asyncio.Task) -> None:
|
||||||
"""Handle completion of an extraction task."""
|
"""Handle completion of an extraction task."""
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ class PlayerState:
|
|||||||
"duration": self.current_sound_duration,
|
"duration": self.current_sound_duration,
|
||||||
"index": self.current_sound_index,
|
"index": self.current_sound_index,
|
||||||
"current_sound": self._serialize_sound(self.current_sound),
|
"current_sound": self._serialize_sound(self.current_sound),
|
||||||
"playlist": {
|
"playlist": (
|
||||||
|
{
|
||||||
"id": self.playlist_id,
|
"id": self.playlist_id,
|
||||||
"name": self.playlist_name,
|
"name": self.playlist_name,
|
||||||
"length": self.playlist_length,
|
"length": self.playlist_length,
|
||||||
@@ -76,7 +77,10 @@ class PlayerState:
|
|||||||
"sounds": [
|
"sounds": [
|
||||||
self._serialize_sound(sound) for sound in self.playlist_sounds
|
self._serialize_sound(sound) for sound in self.playlist_sounds
|
||||||
],
|
],
|
||||||
} if self.playlist_id else None,
|
}
|
||||||
|
if self.playlist_id
|
||||||
|
else None
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _serialize_sound(self, sound: Sound | None) -> dict[str, Any] | None:
|
def _serialize_sound(self, sound: Sound | None) -> dict[str, Any] | None:
|
||||||
@@ -103,6 +107,14 @@ class PlayerService:
|
|||||||
self.db_session_factory = db_session_factory
|
self.db_session_factory = db_session_factory
|
||||||
self.state = PlayerState()
|
self.state = PlayerState()
|
||||||
self._vlc_instance = vlc.Instance()
|
self._vlc_instance = vlc.Instance()
|
||||||
|
|
||||||
|
if self._vlc_instance is None:
|
||||||
|
msg = (
|
||||||
|
"VLC instance could not be created. "
|
||||||
|
"Ensure VLC is installed and accessible."
|
||||||
|
)
|
||||||
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
self._player = self._vlc_instance.media_player_new()
|
self._player = self._vlc_instance.media_player_new()
|
||||||
self._is_running = False
|
self._is_running = False
|
||||||
self._position_thread: threading.Thread | None = None
|
self._position_thread: threading.Thread | None = None
|
||||||
@@ -125,7 +137,8 @@ class PlayerService:
|
|||||||
|
|
||||||
# Start position tracking thread
|
# Start position tracking thread
|
||||||
self._position_thread = threading.Thread(
|
self._position_thread = threading.Thread(
|
||||||
target=self._position_tracker, daemon=True,
|
target=self._position_tracker,
|
||||||
|
daemon=True,
|
||||||
)
|
)
|
||||||
self._position_thread.start()
|
self._position_thread.start()
|
||||||
|
|
||||||
@@ -155,9 +168,9 @@ class PlayerService:
|
|||||||
"""Play audio at specified index or current position."""
|
"""Play audio at specified index or current position."""
|
||||||
# Check if we're resuming from pause
|
# Check if we're resuming from pause
|
||||||
is_resuming = (
|
is_resuming = (
|
||||||
index is None and
|
index is None
|
||||||
self.state.status == PlayerStatus.PAUSED and
|
and self.state.status == PlayerStatus.PAUSED
|
||||||
self.state.current_sound is not None
|
and self.state.current_sound is not None
|
||||||
)
|
)
|
||||||
|
|
||||||
if is_resuming:
|
if is_resuming:
|
||||||
@@ -179,7 +192,14 @@ class PlayerService:
|
|||||||
}
|
}
|
||||||
|
|
||||||
await self._broadcast_state()
|
await self._broadcast_state()
|
||||||
logger.info("Resumed playing sound: %s", self.state.current_sound.name)
|
logger.info(
|
||||||
|
"Resumed playing sound: %s",
|
||||||
|
(
|
||||||
|
self.state.current_sound.name
|
||||||
|
if self.state.current_sound
|
||||||
|
else "Unknown"
|
||||||
|
),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.error("Failed to resume playback: VLC error code %s", result)
|
logger.error("Failed to resume playback: VLC error code %s", result)
|
||||||
return
|
return
|
||||||
@@ -204,6 +224,10 @@ class PlayerService:
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Load and play media (new track)
|
# Load and play media (new track)
|
||||||
|
if self._vlc_instance is None:
|
||||||
|
logger.error("VLC instance is not initialized. Cannot play media.")
|
||||||
|
return
|
||||||
|
|
||||||
media = self._vlc_instance.media_new(str(sound_path))
|
media = self._vlc_instance.media_new(str(sound_path))
|
||||||
self._player.set_media(media)
|
self._player.set_media(media)
|
||||||
|
|
||||||
@@ -354,7 +378,9 @@ class PlayerService:
|
|||||||
and previous_playlist_id != current_playlist.id
|
and previous_playlist_id != current_playlist.id
|
||||||
):
|
):
|
||||||
await self._handle_playlist_id_changed(
|
await self._handle_playlist_id_changed(
|
||||||
previous_playlist_id, current_playlist.id, sounds,
|
previous_playlist_id,
|
||||||
|
current_playlist.id,
|
||||||
|
sounds,
|
||||||
)
|
)
|
||||||
elif previous_current_sound_id:
|
elif previous_current_sound_id:
|
||||||
await self._handle_same_playlist_track_check(
|
await self._handle_same_playlist_track_check(
|
||||||
@@ -432,7 +458,9 @@ class PlayerService:
|
|||||||
self._clear_current_track()
|
self._clear_current_track()
|
||||||
|
|
||||||
def _update_playlist_state(
|
def _update_playlist_state(
|
||||||
self, current_playlist: Playlist, sounds: list[Sound],
|
self,
|
||||||
|
current_playlist: Playlist,
|
||||||
|
sounds: list[Sound],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Update basic playlist state information."""
|
"""Update basic playlist state information."""
|
||||||
self.state.playlist_id = current_playlist.id
|
self.state.playlist_id = current_playlist.id
|
||||||
@@ -464,7 +492,6 @@ class PlayerService:
|
|||||||
"""Get current player state."""
|
"""Get current player state."""
|
||||||
return self.state.to_dict()
|
return self.state.to_dict()
|
||||||
|
|
||||||
|
|
||||||
def _get_next_index(self, current_index: int) -> int | None:
|
def _get_next_index(self, current_index: int) -> int | None:
|
||||||
"""Get next track index based on current mode."""
|
"""Get next track index based on current mode."""
|
||||||
if not self.state.playlist_sounds:
|
if not self.state.playlist_sounds:
|
||||||
@@ -497,11 +524,7 @@ class PlayerService:
|
|||||||
prev_index = current_index - 1
|
prev_index = current_index - 1
|
||||||
|
|
||||||
if prev_index < 0:
|
if prev_index < 0:
|
||||||
return (
|
return playlist_length - 1 if self.state.mode == PlayerMode.LOOP else None
|
||||||
playlist_length - 1
|
|
||||||
if self.state.mode == PlayerMode.LOOP
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
return prev_index
|
return prev_index
|
||||||
|
|
||||||
def _position_tracker(self) -> None:
|
def _position_tracker(self) -> None:
|
||||||
@@ -535,10 +558,7 @@ class PlayerService:
|
|||||||
|
|
||||||
def _update_play_time(self) -> None:
|
def _update_play_time(self) -> None:
|
||||||
"""Update play time tracking for current sound."""
|
"""Update play time tracking for current sound."""
|
||||||
if (
|
if not self.state.current_sound_id or self.state.status != PlayerStatus.PLAYING:
|
||||||
not self.state.current_sound_id
|
|
||||||
or self.state.status != PlayerStatus.PLAYING
|
|
||||||
):
|
|
||||||
return
|
return
|
||||||
|
|
||||||
sound_id = self.state.current_sound_id
|
sound_id = self.state.current_sound_id
|
||||||
@@ -577,10 +597,8 @@ class PlayerService:
|
|||||||
sound_id,
|
sound_id,
|
||||||
tracking["total_time"],
|
tracking["total_time"],
|
||||||
self.state.current_sound_duration,
|
self.state.current_sound_duration,
|
||||||
(
|
(tracking["total_time"] / self.state.current_sound_duration)
|
||||||
tracking["total_time"]
|
* 100,
|
||||||
/ self.state.current_sound_duration
|
|
||||||
) * 100,
|
|
||||||
)
|
)
|
||||||
self._schedule_async_task(self._record_play_count(sound_id))
|
self._schedule_async_task(self._record_play_count(sound_id))
|
||||||
|
|
||||||
@@ -596,7 +614,8 @@ class PlayerService:
|
|||||||
if sound:
|
if sound:
|
||||||
old_count = sound.play_count
|
old_count = sound.play_count
|
||||||
await sound_repo.update(
|
await sound_repo.update(
|
||||||
sound, {"play_count": sound.play_count + 1},
|
sound,
|
||||||
|
{"play_count": sound.play_count + 1},
|
||||||
)
|
)
|
||||||
logger.info(
|
logger.info(
|
||||||
"Updated sound %s play_count: %s -> %s",
|
"Updated sound %s play_count: %s -> %s",
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ class PlaylistService:
|
|||||||
name: str,
|
name: str,
|
||||||
description: str | None = None,
|
description: str | None = None,
|
||||||
genre: str | None = None,
|
genre: str | None = None,
|
||||||
|
*,
|
||||||
is_main: bool = False,
|
is_main: bool = False,
|
||||||
is_current: bool = False,
|
is_current: bool = False,
|
||||||
is_deletable: bool = True,
|
is_deletable: bool = True,
|
||||||
@@ -104,6 +105,7 @@ class PlaylistService:
|
|||||||
self,
|
self,
|
||||||
playlist_id: int,
|
playlist_id: int,
|
||||||
user_id: int,
|
user_id: int,
|
||||||
|
*,
|
||||||
name: str | None = None,
|
name: str | None = None,
|
||||||
description: str | None = None,
|
description: str | None = None,
|
||||||
genre: str | None = None,
|
genre: str | None = None,
|
||||||
@@ -179,7 +181,11 @@ class PlaylistService:
|
|||||||
return await self.playlist_repo.get_playlist_sounds(playlist_id)
|
return await self.playlist_repo.get_playlist_sounds(playlist_id)
|
||||||
|
|
||||||
async def add_sound_to_playlist(
|
async def add_sound_to_playlist(
|
||||||
self, playlist_id: int, sound_id: int, user_id: int, position: int | None = None,
|
self,
|
||||||
|
playlist_id: int,
|
||||||
|
sound_id: int,
|
||||||
|
user_id: int,
|
||||||
|
position: int | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Add a sound to a playlist."""
|
"""Add a sound to a playlist."""
|
||||||
# Verify playlist exists
|
# Verify playlist exists
|
||||||
@@ -202,11 +208,17 @@ class PlaylistService:
|
|||||||
|
|
||||||
await self.playlist_repo.add_sound_to_playlist(playlist_id, sound_id, position)
|
await self.playlist_repo.add_sound_to_playlist(playlist_id, sound_id, position)
|
||||||
logger.info(
|
logger.info(
|
||||||
"Added sound %s to playlist %s for user %s", sound_id, playlist_id, user_id,
|
"Added sound %s to playlist %s for user %s",
|
||||||
|
sound_id,
|
||||||
|
playlist_id,
|
||||||
|
user_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def remove_sound_from_playlist(
|
async def remove_sound_from_playlist(
|
||||||
self, playlist_id: int, sound_id: int, user_id: int,
|
self,
|
||||||
|
playlist_id: int,
|
||||||
|
sound_id: int,
|
||||||
|
user_id: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Remove a sound from a playlist."""
|
"""Remove a sound from a playlist."""
|
||||||
# Verify playlist exists
|
# Verify playlist exists
|
||||||
@@ -228,7 +240,10 @@ class PlaylistService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def reorder_playlist_sounds(
|
async def reorder_playlist_sounds(
|
||||||
self, playlist_id: int, user_id: int, sound_positions: list[tuple[int, int]],
|
self,
|
||||||
|
playlist_id: int,
|
||||||
|
user_id: int,
|
||||||
|
sound_positions: list[tuple[int, int]],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Reorder sounds in a playlist."""
|
"""Reorder sounds in a playlist."""
|
||||||
# Verify playlist exists
|
# Verify playlist exists
|
||||||
@@ -262,7 +277,8 @@ class PlaylistService:
|
|||||||
await self._unset_current_playlist(user_id)
|
await self._unset_current_playlist(user_id)
|
||||||
await self._set_main_as_current(user_id)
|
await self._set_main_as_current(user_id)
|
||||||
logger.info(
|
logger.info(
|
||||||
"Unset current playlist and set main as current for user %s", user_id,
|
"Unset current playlist and set main as current for user %s",
|
||||||
|
user_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_playlist_stats(self, playlist_id: int) -> dict[str, Any]:
|
async def get_playlist_stats(self, playlist_id: int) -> dict[str, Any]:
|
||||||
@@ -286,11 +302,13 @@ class PlaylistService:
|
|||||||
main_playlist = await self.get_main_playlist()
|
main_playlist = await self.get_main_playlist()
|
||||||
|
|
||||||
if main_playlist.id is None:
|
if main_playlist.id is None:
|
||||||
raise ValueError("Main playlist has no ID")
|
msg = "Main playlist has no ID, cannot add sound"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
# Check if sound is already in main playlist
|
# Check if sound is already in main playlist
|
||||||
if not await self.playlist_repo.is_sound_in_playlist(
|
if not await self.playlist_repo.is_sound_in_playlist(
|
||||||
main_playlist.id, sound_id,
|
main_playlist.id,
|
||||||
|
sound_id,
|
||||||
):
|
):
|
||||||
await self.playlist_repo.add_sound_to_playlist(main_playlist.id, sound_id)
|
await self.playlist_repo.add_sound_to_playlist(main_playlist.id, sound_id)
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ logger = logging.getLogger(__name__)
|
|||||||
class SocketManager:
|
class SocketManager:
|
||||||
"""Manages WebSocket connections and user rooms."""
|
"""Manages WebSocket connections and user rooms."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
|
"""Initialize the SocketManager with a Socket.IO server."""
|
||||||
self.sio = socketio.AsyncServer(
|
self.sio = socketio.AsyncServer(
|
||||||
cors_allowed_origins=["http://localhost:8001"],
|
cors_allowed_origins=["http://localhost:8001"],
|
||||||
logger=True,
|
logger=True,
|
||||||
@@ -27,20 +28,20 @@ class SocketManager:
|
|||||||
|
|
||||||
self._setup_handlers()
|
self._setup_handlers()
|
||||||
|
|
||||||
def _setup_handlers(self):
|
def _setup_handlers(self) -> None:
|
||||||
"""Set up socket event handlers."""
|
"""Set up socket event handlers."""
|
||||||
|
|
||||||
@self.sio.event
|
@self.sio.event
|
||||||
async def connect(sid, environ, auth=None):
|
async def connect(sid: str, environ: dict) -> None:
|
||||||
"""Handle client connection."""
|
"""Handle client connection."""
|
||||||
logger.info(f"Client {sid} attempting to connect")
|
logger.info("Client %s attempting to connect", sid)
|
||||||
|
|
||||||
# Extract access token from cookies
|
# Extract access token from cookies
|
||||||
cookie_header = environ.get("HTTP_COOKIE", "")
|
cookie_header = environ.get("HTTP_COOKIE", "")
|
||||||
access_token = extract_access_token_from_cookies(cookie_header)
|
access_token = extract_access_token_from_cookies(cookie_header)
|
||||||
|
|
||||||
if not access_token:
|
if not access_token:
|
||||||
logger.warning(f"Client {sid} connecting without access token")
|
logger.warning("Client %s connecting without access token", sid)
|
||||||
await self.sio.disconnect(sid)
|
await self.sio.disconnect(sid)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -50,13 +51,13 @@ class SocketManager:
|
|||||||
user_id = payload.get("sub")
|
user_id = payload.get("sub")
|
||||||
|
|
||||||
if not user_id:
|
if not user_id:
|
||||||
logger.warning(f"Client {sid} token missing user ID")
|
logger.warning("Client %s token missing user ID", sid)
|
||||||
await self.sio.disconnect(sid)
|
await self.sio.disconnect(sid)
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.info(f"User {user_id} connected with socket {sid}")
|
logger.info("User %s connected with socket %s", user_id, sid)
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.warning(f"Client {sid} invalid token: {e}")
|
logger.exception("Client %s invalid token", sid)
|
||||||
await self.sio.disconnect(sid)
|
await self.sio.disconnect(sid)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -70,7 +71,7 @@ class SocketManager:
|
|||||||
# Update room tracking
|
# Update room tracking
|
||||||
self.user_rooms[user_id] = room_id
|
self.user_rooms[user_id] = room_id
|
||||||
|
|
||||||
logger.info(f"User {user_id} joined room {room_id}")
|
logger.info("User %s joined room %s", user_id, room_id)
|
||||||
|
|
||||||
# Send welcome message to user
|
# Send welcome message to user
|
||||||
await self.sio.emit(
|
await self.sio.emit(
|
||||||
@@ -84,33 +85,33 @@ class SocketManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@self.sio.event
|
@self.sio.event
|
||||||
async def disconnect(sid):
|
async def disconnect(sid: str) -> None:
|
||||||
"""Handle client disconnection."""
|
"""Handle client disconnection."""
|
||||||
user_id = self.socket_users.get(sid)
|
user_id = self.socket_users.get(sid)
|
||||||
|
|
||||||
if user_id:
|
if user_id:
|
||||||
logger.info(f"User {user_id} disconnected (socket {sid})")
|
logger.info("User %s disconnected (socket %s)", user_id, sid)
|
||||||
# Clean up mappings
|
# Clean up mappings
|
||||||
del self.socket_users[sid]
|
del self.socket_users[sid]
|
||||||
if user_id in self.user_rooms:
|
if user_id in self.user_rooms:
|
||||||
del self.user_rooms[user_id]
|
del self.user_rooms[user_id]
|
||||||
else:
|
else:
|
||||||
logger.info(f"Unknown client {sid} disconnected")
|
logger.info("Unknown client %s disconnected", sid)
|
||||||
|
|
||||||
async def send_to_user(self, user_id: str, event: str, data: dict):
|
async def send_to_user(self, user_id: str, event: str, data: dict) -> bool:
|
||||||
"""Send a message to a specific user's room."""
|
"""Send a message to a specific user's room."""
|
||||||
room_id = self.user_rooms.get(user_id)
|
room_id = self.user_rooms.get(user_id)
|
||||||
if room_id:
|
if room_id:
|
||||||
await self.sio.emit(event, data, room=room_id)
|
await self.sio.emit(event, data, room=room_id)
|
||||||
logger.debug(f"Sent {event} to user {user_id} in room {room_id}")
|
logger.debug("Sent %s to user %s in room %s", event, user_id, room_id)
|
||||||
return True
|
return True
|
||||||
logger.warning(f"User {user_id} not found in any room")
|
logger.warning("User %s not found in any room", user_id)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def broadcast_to_all(self, event: str, data: dict):
|
async def broadcast_to_all(self, event: str, data: dict) -> None:
|
||||||
"""Broadcast a message to all connected users."""
|
"""Broadcast a message to all connected users."""
|
||||||
await self.sio.emit(event, data)
|
await self.sio.emit(event, data)
|
||||||
logger.info(f"Broadcasted {event} to all users")
|
logger.info("Broadcasted %s to all users", event)
|
||||||
|
|
||||||
def get_connected_users(self) -> list:
|
def get_connected_users(self) -> list:
|
||||||
"""Get list of currently connected user IDs."""
|
"""Get list of currently connected user IDs."""
|
||||||
|
|||||||
@@ -153,7 +153,9 @@ class SoundNormalizerService:
|
|||||||
"""Normalize audio using two-pass loudnorm for better quality."""
|
"""Normalize audio using two-pass loudnorm for better quality."""
|
||||||
try:
|
try:
|
||||||
logger.info(
|
logger.info(
|
||||||
"Starting two-pass normalization: %s -> %s", input_path, output_path,
|
"Starting two-pass normalization: %s -> %s",
|
||||||
|
input_path,
|
||||||
|
output_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
# First pass: analyze
|
# First pass: analyze
|
||||||
@@ -177,7 +179,7 @@ class SoundNormalizerService:
|
|||||||
result = ffmpeg.run(stream, capture_stderr=True, quiet=True)
|
result = ffmpeg.run(stream, capture_stderr=True, quiet=True)
|
||||||
analysis_output = result[1].decode("utf-8")
|
analysis_output = result[1].decode("utf-8")
|
||||||
except ffmpeg.Error as e:
|
except ffmpeg.Error as e:
|
||||||
logger.error(
|
logger.exception(
|
||||||
"FFmpeg first pass failed for %s. Stdout: %s, Stderr: %s",
|
"FFmpeg first pass failed for %s. Stdout: %s, Stderr: %s",
|
||||||
input_path,
|
input_path,
|
||||||
e.stdout.decode() if e.stdout else "None",
|
e.stdout.decode() if e.stdout else "None",
|
||||||
@@ -193,9 +195,11 @@ class SoundNormalizerService:
|
|||||||
json_match = re.search(r'\{[^{}]*"input_i"[^{}]*\}', analysis_output)
|
json_match = re.search(r'\{[^{}]*"input_i"[^{}]*\}', analysis_output)
|
||||||
if not json_match:
|
if not json_match:
|
||||||
logger.error(
|
logger.error(
|
||||||
"Could not find JSON in loudnorm output: %s", analysis_output,
|
"Could not find JSON in loudnorm output: %s",
|
||||||
|
analysis_output,
|
||||||
)
|
)
|
||||||
raise ValueError("Could not extract loudnorm analysis data")
|
msg = "Could not find JSON in loudnorm output"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
logger.debug("Found JSON match: %s", json_match.group())
|
logger.debug("Found JSON match: %s", json_match.group())
|
||||||
analysis_data = json.loads(json_match.group())
|
analysis_data = json.loads(json_match.group())
|
||||||
@@ -211,7 +215,10 @@ class SoundNormalizerService:
|
|||||||
]:
|
]:
|
||||||
if str(analysis_data.get(key, "")).lower() in invalid_values:
|
if str(analysis_data.get(key, "")).lower() in invalid_values:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Invalid analysis value for %s: %s. Falling back to one-pass normalization.",
|
(
|
||||||
|
"Invalid analysis value for %s: %s. "
|
||||||
|
"Falling back to one-pass normalization."
|
||||||
|
),
|
||||||
key,
|
key,
|
||||||
analysis_data.get(key),
|
analysis_data.get(key),
|
||||||
)
|
)
|
||||||
@@ -252,7 +259,7 @@ class SoundNormalizerService:
|
|||||||
ffmpeg.run(stream, quiet=True, overwrite_output=True)
|
ffmpeg.run(stream, quiet=True, overwrite_output=True)
|
||||||
logger.info("Two-pass normalization completed: %s", output_path)
|
logger.info("Two-pass normalization completed: %s", output_path)
|
||||||
except ffmpeg.Error as e:
|
except ffmpeg.Error as e:
|
||||||
logger.error(
|
logger.exception(
|
||||||
"FFmpeg second pass failed for %s. Stdout: %s, Stderr: %s",
|
"FFmpeg second pass failed for %s. Stdout: %s, Stderr: %s",
|
||||||
input_path,
|
input_path,
|
||||||
e.stdout.decode() if e.stdout else "None",
|
e.stdout.decode() if e.stdout else "None",
|
||||||
@@ -267,12 +274,14 @@ class SoundNormalizerService:
|
|||||||
async def normalize_sound(
|
async def normalize_sound(
|
||||||
self,
|
self,
|
||||||
sound: Sound,
|
sound: Sound,
|
||||||
|
*,
|
||||||
force: bool = False,
|
force: bool = False,
|
||||||
one_pass: bool | None = None,
|
one_pass: bool | None = None,
|
||||||
sound_data: dict | None = None,
|
sound_data: dict | None = None,
|
||||||
) -> NormalizationInfo:
|
) -> NormalizationInfo:
|
||||||
"""Normalize a single sound."""
|
"""Normalize a single sound."""
|
||||||
# Use provided sound_data to avoid detached instance issues, or capture from sound
|
# Use provided sound_data to avoid detached instance issues,
|
||||||
|
# or capture from sound
|
||||||
if sound_data:
|
if sound_data:
|
||||||
filename = sound_data["filename"]
|
filename = sound_data["filename"]
|
||||||
sound_id = sound_data["id"]
|
sound_id = sound_data["id"]
|
||||||
@@ -391,6 +400,7 @@ class SoundNormalizerService:
|
|||||||
|
|
||||||
async def normalize_all_sounds(
|
async def normalize_all_sounds(
|
||||||
self,
|
self,
|
||||||
|
*,
|
||||||
force: bool = False,
|
force: bool = False,
|
||||||
one_pass: bool | None = None,
|
one_pass: bool | None = None,
|
||||||
) -> NormalizationResults:
|
) -> NormalizationResults:
|
||||||
@@ -409,7 +419,7 @@ class SoundNormalizerService:
|
|||||||
if force:
|
if force:
|
||||||
# Get all sounds if forcing
|
# Get all sounds if forcing
|
||||||
sounds = []
|
sounds = []
|
||||||
for sound_type in self.type_directories.keys():
|
for sound_type in self.type_directories:
|
||||||
type_sounds = await self.sound_repo.get_by_type(sound_type)
|
type_sounds = await self.sound_repo.get_by_type(sound_type)
|
||||||
sounds.extend(type_sounds)
|
sounds.extend(type_sounds)
|
||||||
else:
|
else:
|
||||||
@@ -419,17 +429,16 @@ class SoundNormalizerService:
|
|||||||
logger.info("Found %d sounds to process", len(sounds))
|
logger.info("Found %d sounds to process", len(sounds))
|
||||||
|
|
||||||
# Capture all sound data upfront to avoid session detachment issues
|
# Capture all sound data upfront to avoid session detachment issues
|
||||||
sound_data_list = []
|
sound_data_list = [
|
||||||
for sound in sounds:
|
|
||||||
sound_data_list.append(
|
|
||||||
{
|
{
|
||||||
"id": sound.id,
|
"id": sound.id,
|
||||||
"filename": sound.filename,
|
"filename": sound.filename,
|
||||||
"type": sound.type,
|
"type": sound.type,
|
||||||
"is_normalized": sound.is_normalized,
|
"is_normalized": sound.is_normalized,
|
||||||
"name": sound.name,
|
"name": sound.name,
|
||||||
},
|
}
|
||||||
)
|
for sound in sounds
|
||||||
|
]
|
||||||
|
|
||||||
# Process each sound using captured data
|
# Process each sound using captured data
|
||||||
for i, sound in enumerate(sounds):
|
for i, sound in enumerate(sounds):
|
||||||
@@ -485,6 +494,7 @@ class SoundNormalizerService:
|
|||||||
async def normalize_sounds_by_type(
|
async def normalize_sounds_by_type(
|
||||||
self,
|
self,
|
||||||
sound_type: str,
|
sound_type: str,
|
||||||
|
*,
|
||||||
force: bool = False,
|
force: bool = False,
|
||||||
one_pass: bool | None = None,
|
one_pass: bool | None = None,
|
||||||
) -> NormalizationResults:
|
) -> NormalizationResults:
|
||||||
@@ -508,17 +518,16 @@ class SoundNormalizerService:
|
|||||||
logger.info("Found %d %s sounds to process", len(sounds), sound_type)
|
logger.info("Found %d %s sounds to process", len(sounds), sound_type)
|
||||||
|
|
||||||
# Capture all sound data upfront to avoid session detachment issues
|
# Capture all sound data upfront to avoid session detachment issues
|
||||||
sound_data_list = []
|
sound_data_list = [
|
||||||
for sound in sounds:
|
|
||||||
sound_data_list.append(
|
|
||||||
{
|
{
|
||||||
"id": sound.id,
|
"id": sound.id,
|
||||||
"filename": sound.filename,
|
"filename": sound.filename,
|
||||||
"type": sound.type,
|
"type": sound.type,
|
||||||
"is_normalized": sound.is_normalized,
|
"is_normalized": sound.is_normalized,
|
||||||
"name": sound.name,
|
"name": sound.name,
|
||||||
},
|
}
|
||||||
)
|
for sound in sounds
|
||||||
|
]
|
||||||
|
|
||||||
# Process each sound using captured data
|
# Process each sound using captured data
|
||||||
for i, sound in enumerate(sounds):
|
for i, sound in enumerate(sounds):
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ class VLCPlayerService:
|
|||||||
"""Service for launching VLC instances via subprocess to play sounds."""
|
"""Service for launching VLC instances via subprocess to play sounds."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, db_session_factory: Callable[[], AsyncSession] | None = None,
|
self,
|
||||||
|
db_session_factory: Callable[[], AsyncSession] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the VLC player service."""
|
"""Initialize the VLC player service."""
|
||||||
self.vlc_executable = self._find_vlc_executable()
|
self.vlc_executable = self._find_vlc_executable()
|
||||||
@@ -52,7 +53,7 @@ class VLCPlayerService:
|
|||||||
# For "vlc", try to find it in PATH
|
# For "vlc", try to find it in PATH
|
||||||
if path == "vlc":
|
if path == "vlc":
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["which", "vlc"],
|
["which", "vlc"], # noqa: S607
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
check=False,
|
check=False,
|
||||||
text=True,
|
text=True,
|
||||||
@@ -112,13 +113,19 @@ class VLCPlayerService:
|
|||||||
|
|
||||||
# Record play count and emit event
|
# Record play count and emit event
|
||||||
if self.db_session_factory and sound.id:
|
if self.db_session_factory and sound.id:
|
||||||
asyncio.create_task(self._record_play_count(sound.id, sound.name))
|
task = asyncio.create_task(
|
||||||
|
self._record_play_count(sound.id, sound.name),
|
||||||
return True
|
)
|
||||||
|
# Store reference to prevent garbage collection
|
||||||
|
self._background_tasks = getattr(self, "_background_tasks", set())
|
||||||
|
self._background_tasks.add(task)
|
||||||
|
task.add_done_callback(self._background_tasks.discard)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Failed to launch VLC for sound %s", sound.name)
|
logger.exception("Failed to launch VLC for sound %s", sound.name)
|
||||||
return False
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
async def stop_all_vlc_instances(self) -> dict[str, Any]:
|
async def stop_all_vlc_instances(self) -> dict[str, Any]:
|
||||||
"""Stop all running VLC processes by killing them.
|
"""Stop all running VLC processes by killing them.
|
||||||
@@ -287,7 +294,8 @@ class VLCPlayerService:
|
|||||||
logger.info("Broadcasted sound_played event for sound %s", sound_id)
|
logger.info("Broadcasted sound_played event for sound %s", sound_id)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception(
|
logger.exception(
|
||||||
"Failed to broadcast sound_played event for sound %s", sound_id,
|
"Failed to broadcast sound_played event for sound %s",
|
||||||
|
sound_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -297,7 +305,6 @@ class VLCPlayerService:
|
|||||||
await session.close()
|
await session.close()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Global VLC player service instance
|
# Global VLC player service instance
|
||||||
vlc_player_service: VLCPlayerService | None = None
|
vlc_player_service: VLCPlayerService | None = None
|
||||||
|
|
||||||
@@ -310,3 +317,4 @@ def get_vlc_player_service(
|
|||||||
if vlc_player_service is None:
|
if vlc_player_service is None:
|
||||||
vlc_player_service = VLCPlayerService(db_session_factory)
|
vlc_player_service = VLCPlayerService(db_session_factory)
|
||||||
return vlc_player_service
|
return vlc_player_service
|
||||||
|
return vlc_player_service
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ def requires_credits(
|
|||||||
|
|
||||||
# Validate credits before execution
|
# Validate credits before execution
|
||||||
await credit_service.validate_and_reserve_credits(
|
await credit_service.validate_and_reserve_credits(
|
||||||
user_id, action_type, metadata,
|
user_id, action_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Execute the function
|
# Execute the function
|
||||||
@@ -86,7 +86,7 @@ def requires_credits(
|
|||||||
finally:
|
finally:
|
||||||
# Deduct credits based on success
|
# Deduct credits based on success
|
||||||
await credit_service.deduct_credits(
|
await credit_service.deduct_credits(
|
||||||
user_id, action_type, success, metadata,
|
user_id, action_type, success=success, metadata=metadata,
|
||||||
)
|
)
|
||||||
|
|
||||||
return wrapper # type: ignore[return-value]
|
return wrapper # type: ignore[return-value]
|
||||||
@@ -173,7 +173,7 @@ class CreditManager:
|
|||||||
async def __aenter__(self) -> "CreditManager":
|
async def __aenter__(self) -> "CreditManager":
|
||||||
"""Enter context manager - validate credits."""
|
"""Enter context manager - validate credits."""
|
||||||
await self.credit_service.validate_and_reserve_credits(
|
await self.credit_service.validate_and_reserve_credits(
|
||||||
self.user_id, self.action_type, self.metadata,
|
self.user_id, self.action_type,
|
||||||
)
|
)
|
||||||
self.validated = True
|
self.validated = True
|
||||||
return self
|
return self
|
||||||
@@ -189,7 +189,7 @@ class CreditManager:
|
|||||||
# If no exception occurred, consider it successful
|
# If no exception occurred, consider it successful
|
||||||
success = exc_type is None and self.success
|
success = exc_type is None and self.success
|
||||||
await self.credit_service.deduct_credits(
|
await self.credit_service.deduct_credits(
|
||||||
self.user_id, self.action_type, success, self.metadata,
|
self.user_id, self.action_type, success=success, metadata=self.metadata,
|
||||||
)
|
)
|
||||||
|
|
||||||
def mark_success(self) -> None:
|
def mark_success(self) -> None:
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ class TestCreditService:
|
|||||||
mock_socket_manager.send_to_user = AsyncMock()
|
mock_socket_manager.send_to_user = AsyncMock()
|
||||||
|
|
||||||
transaction = await credit_service.deduct_credits(
|
transaction = await credit_service.deduct_credits(
|
||||||
1, CreditActionType.VLC_PLAY_SOUND, True, {"test": "data"},
|
1, CreditActionType.VLC_PLAY_SOUND, success=True, metadata={"test": "data"},
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verify user credits were updated
|
# Verify user credits were updated
|
||||||
@@ -214,7 +214,7 @@ class TestCreditService:
|
|||||||
mock_socket_manager.send_to_user = AsyncMock()
|
mock_socket_manager.send_to_user = AsyncMock()
|
||||||
|
|
||||||
transaction = await credit_service.deduct_credits(
|
transaction = await credit_service.deduct_credits(
|
||||||
1, CreditActionType.VLC_PLAY_SOUND, False, # Action failed
|
1, CreditActionType.VLC_PLAY_SOUND, success=False, # Action failed
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verify user credits were NOT updated (action requires success)
|
# Verify user credits were NOT updated (action requires success)
|
||||||
@@ -256,7 +256,7 @@ class TestCreditService:
|
|||||||
|
|
||||||
with pytest.raises(InsufficientCreditsError):
|
with pytest.raises(InsufficientCreditsError):
|
||||||
await credit_service.deduct_credits(
|
await credit_service.deduct_credits(
|
||||||
1, CreditActionType.VLC_PLAY_SOUND, True,
|
1, CreditActionType.VLC_PLAY_SOUND, success=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verify no socket event was emitted since credits could not be deducted
|
# Verify no socket event was emitted since credits could not be deducted
|
||||||
|
|||||||
@@ -45,10 +45,10 @@ class TestRequiresCreditsDecorator:
|
|||||||
|
|
||||||
assert result == "Success: test"
|
assert result == "Success: test"
|
||||||
mock_credit_service.validate_and_reserve_credits.assert_called_once_with(
|
mock_credit_service.validate_and_reserve_credits.assert_called_once_with(
|
||||||
123, CreditActionType.VLC_PLAY_SOUND, None,
|
123, CreditActionType.VLC_PLAY_SOUND,
|
||||||
)
|
)
|
||||||
mock_credit_service.deduct_credits.assert_called_once_with(
|
mock_credit_service.deduct_credits.assert_called_once_with(
|
||||||
123, CreditActionType.VLC_PLAY_SOUND, True, None,
|
123, CreditActionType.VLC_PLAY_SOUND, success=True, metadata=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -70,10 +70,10 @@ class TestRequiresCreditsDecorator:
|
|||||||
await test_action(user_id=123, sound_name="test.mp3")
|
await test_action(user_id=123, sound_name="test.mp3")
|
||||||
|
|
||||||
mock_credit_service.validate_and_reserve_credits.assert_called_once_with(
|
mock_credit_service.validate_and_reserve_credits.assert_called_once_with(
|
||||||
123, CreditActionType.VLC_PLAY_SOUND, {"sound_name": "test.mp3"},
|
123, CreditActionType.VLC_PLAY_SOUND,
|
||||||
)
|
)
|
||||||
mock_credit_service.deduct_credits.assert_called_once_with(
|
mock_credit_service.deduct_credits.assert_called_once_with(
|
||||||
123, CreditActionType.VLC_PLAY_SOUND, True, {"sound_name": "test.mp3"},
|
123, CreditActionType.VLC_PLAY_SOUND, success=True, metadata={"sound_name": "test.mp3"},
|
||||||
)
|
)
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -92,7 +92,7 @@ class TestRequiresCreditsDecorator:
|
|||||||
|
|
||||||
assert result is False
|
assert result is False
|
||||||
mock_credit_service.deduct_credits.assert_called_once_with(
|
mock_credit_service.deduct_credits.assert_called_once_with(
|
||||||
123, CreditActionType.VLC_PLAY_SOUND, False, None,
|
123, CreditActionType.VLC_PLAY_SOUND, success=False, metadata=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -111,7 +111,7 @@ class TestRequiresCreditsDecorator:
|
|||||||
await test_action(user_id=123)
|
await test_action(user_id=123)
|
||||||
|
|
||||||
mock_credit_service.deduct_credits.assert_called_once_with(
|
mock_credit_service.deduct_credits.assert_called_once_with(
|
||||||
123, CreditActionType.VLC_PLAY_SOUND, False, None,
|
123, CreditActionType.VLC_PLAY_SOUND, success=False, metadata=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -149,7 +149,7 @@ class TestRequiresCreditsDecorator:
|
|||||||
|
|
||||||
assert result == "test"
|
assert result == "test"
|
||||||
mock_credit_service.validate_and_reserve_credits.assert_called_once_with(
|
mock_credit_service.validate_and_reserve_credits.assert_called_once_with(
|
||||||
123, CreditActionType.VLC_PLAY_SOUND, None,
|
123, CreditActionType.VLC_PLAY_SOUND,
|
||||||
)
|
)
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -228,10 +228,10 @@ class TestCreditManager:
|
|||||||
manager.mark_success()
|
manager.mark_success()
|
||||||
|
|
||||||
mock_credit_service.validate_and_reserve_credits.assert_called_once_with(
|
mock_credit_service.validate_and_reserve_credits.assert_called_once_with(
|
||||||
123, CreditActionType.VLC_PLAY_SOUND, {"test": "data"},
|
123, CreditActionType.VLC_PLAY_SOUND,
|
||||||
)
|
)
|
||||||
mock_credit_service.deduct_credits.assert_called_once_with(
|
mock_credit_service.deduct_credits.assert_called_once_with(
|
||||||
123, CreditActionType.VLC_PLAY_SOUND, True, {"test": "data"},
|
123, CreditActionType.VLC_PLAY_SOUND, success=True, metadata={"test": "data"},
|
||||||
)
|
)
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -246,7 +246,7 @@ class TestCreditManager:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
mock_credit_service.deduct_credits.assert_called_once_with(
|
mock_credit_service.deduct_credits.assert_called_once_with(
|
||||||
123, CreditActionType.VLC_PLAY_SOUND, False, None,
|
123, CreditActionType.VLC_PLAY_SOUND, success=False, metadata=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -261,7 +261,7 @@ class TestCreditManager:
|
|||||||
raise ValueError("Test error")
|
raise ValueError("Test error")
|
||||||
|
|
||||||
mock_credit_service.deduct_credits.assert_called_once_with(
|
mock_credit_service.deduct_credits.assert_called_once_with(
|
||||||
123, CreditActionType.VLC_PLAY_SOUND, False, None,
|
123, CreditActionType.VLC_PLAY_SOUND, success=False, metadata=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
Reference in New Issue
Block a user