Compare commits
2 Commits
b66b8e36bb
...
12243b1424
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12243b1424 | ||
|
|
f7197a89a7 |
@@ -249,3 +249,21 @@ async def get_state(
|
|||||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
detail="Failed to get player state",
|
detail="Failed to get player state",
|
||||||
) from e
|
) from e
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/play-next/{sound_id}")
|
||||||
|
async def add_to_play_next(
|
||||||
|
sound_id: int,
|
||||||
|
current_user: Annotated[User, Depends(get_current_active_user_flexible)], # noqa: ARG001
|
||||||
|
) -> MessageResponse:
|
||||||
|
"""Add a sound to the play next queue."""
|
||||||
|
try:
|
||||||
|
player = get_player_service()
|
||||||
|
await player.add_to_play_next(sound_id)
|
||||||
|
return MessageResponse(message=f"Added sound {sound_id} to play next queue")
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("Error adding sound to play next queue: %s", sound_id)
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
detail="Failed to add sound to play next queue",
|
||||||
|
) from e
|
||||||
|
|||||||
@@ -49,3 +49,7 @@ class PlayerStateResponse(BaseModel):
|
|||||||
None,
|
None,
|
||||||
description="Current track index in playlist",
|
description="Current track index in playlist",
|
||||||
)
|
)
|
||||||
|
play_next_queue: list[dict[str, Any]] = Field(
|
||||||
|
default_factory=list,
|
||||||
|
description="Play next queue",
|
||||||
|
)
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ from enum import Enum
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import vlc # type: ignore[import-untyped]
|
import vlc # type: ignore[import-untyped]
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
|
from sqlmodel import select
|
||||||
|
|
||||||
from app.core.logging import get_logger
|
from app.core.logging import get_logger
|
||||||
from app.models.playlist import Playlist
|
from app.models.playlist import Playlist
|
||||||
@@ -62,6 +64,7 @@ class PlayerState:
|
|||||||
self.playlist_length: int = 0
|
self.playlist_length: int = 0
|
||||||
self.playlist_duration: int = 0
|
self.playlist_duration: int = 0
|
||||||
self.playlist_sounds: list[Sound] = []
|
self.playlist_sounds: list[Sound] = []
|
||||||
|
self.play_next_queue: list[Sound] = []
|
||||||
|
|
||||||
def to_dict(self) -> dict[str, Any]:
|
def to_dict(self) -> dict[str, Any]:
|
||||||
"""Convert player state to dictionary for serialization."""
|
"""Convert player state to dictionary for serialization."""
|
||||||
@@ -87,6 +90,9 @@ class PlayerState:
|
|||||||
if self.playlist_id
|
if self.playlist_id
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
|
"play_next_queue": [
|
||||||
|
self._serialize_sound(sound) for sound in self.play_next_queue
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
def _serialize_sound(self, sound: Sound | None) -> dict[str, Any] | None:
|
def _serialize_sound(self, sound: Sound | None) -> dict[str, Any] | None:
|
||||||
@@ -342,6 +348,11 @@ class PlayerService:
|
|||||||
|
|
||||||
async def next(self) -> None:
|
async def next(self) -> None:
|
||||||
"""Skip to next track."""
|
"""Skip to next track."""
|
||||||
|
# Check if there's a track in the play_next queue
|
||||||
|
if self.state.play_next_queue:
|
||||||
|
await self._play_next_from_queue()
|
||||||
|
return
|
||||||
|
|
||||||
if not self.state.playlist_sounds:
|
if not self.state.playlist_sounds:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -431,6 +442,52 @@ class PlayerService:
|
|||||||
await self._broadcast_state()
|
await self._broadcast_state()
|
||||||
logger.info("Playback mode set to: %s", mode.value)
|
logger.info("Playback mode set to: %s", mode.value)
|
||||||
|
|
||||||
|
async def add_to_play_next(self, sound_id: int) -> None:
|
||||||
|
"""Add a sound to the play_next queue."""
|
||||||
|
session = self.db_session_factory()
|
||||||
|
try:
|
||||||
|
# Eagerly load extractions to avoid lazy loading issues
|
||||||
|
statement = select(Sound).where(Sound.id == sound_id)
|
||||||
|
statement = statement.options(selectinload(Sound.extractions)) # type: ignore[arg-type]
|
||||||
|
result = await session.exec(statement)
|
||||||
|
sound = result.first()
|
||||||
|
|
||||||
|
if not sound:
|
||||||
|
logger.warning("Sound %s not found for play_next", sound_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.state.play_next_queue.append(sound)
|
||||||
|
await self._broadcast_state()
|
||||||
|
logger.info("Added sound %s to play_next queue", sound.name)
|
||||||
|
finally:
|
||||||
|
await session.close()
|
||||||
|
|
||||||
|
async def _play_next_from_queue(self) -> None:
|
||||||
|
"""Play the first track from the play_next queue."""
|
||||||
|
if not self.state.play_next_queue:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get the first sound from the queue
|
||||||
|
next_sound = self.state.play_next_queue.pop(0)
|
||||||
|
|
||||||
|
# Stop current playback and process play count
|
||||||
|
if self.state.status != PlayerStatus.STOPPED:
|
||||||
|
await self._stop_playback()
|
||||||
|
|
||||||
|
# Set the sound as current (without index since it's from play_next)
|
||||||
|
self.state.current_sound = next_sound
|
||||||
|
self.state.current_sound_id = next_sound.id
|
||||||
|
self.state.current_sound_index = None # No index for play_next tracks
|
||||||
|
|
||||||
|
# Play the sound
|
||||||
|
if not self._validate_sound_file():
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self._load_and_play_media():
|
||||||
|
return
|
||||||
|
|
||||||
|
await self._handle_successful_playback()
|
||||||
|
|
||||||
async def reload_playlist(self) -> None:
|
async def reload_playlist(self) -> None:
|
||||||
"""Reload current playlist from database."""
|
"""Reload current playlist from database."""
|
||||||
session = self.db_session_factory()
|
session = self.db_session_factory()
|
||||||
@@ -519,6 +576,11 @@ class PlayerService:
|
|||||||
current_id,
|
current_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Clear play_next queue when playlist changes
|
||||||
|
if self.state.play_next_queue:
|
||||||
|
logger.info("Clearing play_next queue due to playlist change")
|
||||||
|
self.state.play_next_queue.clear()
|
||||||
|
|
||||||
if self.state.status != PlayerStatus.STOPPED:
|
if self.state.status != PlayerStatus.STOPPED:
|
||||||
await self._stop_playback()
|
await self._stop_playback()
|
||||||
|
|
||||||
@@ -534,6 +596,9 @@ class PlayerService:
|
|||||||
sounds: list[Sound],
|
sounds: list[Sound],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Handle track checking when playlist ID is the same."""
|
"""Handle track checking when playlist ID is the same."""
|
||||||
|
# Remove tracks from play_next queue that are no longer in the playlist
|
||||||
|
self._clean_play_next_queue(sounds)
|
||||||
|
|
||||||
# Find the current track in the new playlist
|
# Find the current track in the new playlist
|
||||||
new_index = self._find_sound_index(previous_sound_id, sounds)
|
new_index = self._find_sound_index(previous_sound_id, sounds)
|
||||||
|
|
||||||
@@ -591,6 +656,29 @@ class PlayerService:
|
|||||||
return i
|
return i
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _clean_play_next_queue(self, playlist_sounds: list[Sound]) -> None:
|
||||||
|
"""Remove tracks from play_next queue that are no longer in the playlist."""
|
||||||
|
if not self.state.play_next_queue:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get IDs of all sounds in the current playlist
|
||||||
|
playlist_sound_ids = {sound.id for sound in playlist_sounds}
|
||||||
|
|
||||||
|
# Filter out tracks that are no longer in the playlist
|
||||||
|
original_length = len(self.state.play_next_queue)
|
||||||
|
self.state.play_next_queue = [
|
||||||
|
sound
|
||||||
|
for sound in self.state.play_next_queue
|
||||||
|
if sound.id in playlist_sound_ids
|
||||||
|
]
|
||||||
|
|
||||||
|
removed_count = original_length - len(self.state.play_next_queue)
|
||||||
|
if removed_count > 0:
|
||||||
|
logger.info(
|
||||||
|
"Removed %s track(s) from play_next queue (no longer in playlist)",
|
||||||
|
removed_count,
|
||||||
|
)
|
||||||
|
|
||||||
def _set_first_track_as_current(self, sounds: list[Sound]) -> None:
|
def _set_first_track_as_current(self, sounds: list[Sound]) -> None:
|
||||||
"""Set the first track as the current track."""
|
"""Set the first track as the current track."""
|
||||||
self.state.current_sound_index = 0
|
self.state.current_sound_index = 0
|
||||||
@@ -780,7 +868,12 @@ class PlayerService:
|
|||||||
"""Handle when a track finishes playing."""
|
"""Handle when a track finishes playing."""
|
||||||
await self._process_play_count()
|
await self._process_play_count()
|
||||||
|
|
||||||
# Auto-advance to next track
|
# Check if there's a track in the play_next queue
|
||||||
|
if self.state.play_next_queue:
|
||||||
|
await self._play_next_from_queue()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Auto-advance to next track in playlist
|
||||||
if self.state.current_sound_index is not None:
|
if self.state.current_sound_index is not None:
|
||||||
next_index = self._get_next_index(self.state.current_sound_index)
|
next_index = self._get_next_index(self.state.current_sound_index)
|
||||||
if next_index is not None:
|
if next_index is not None:
|
||||||
@@ -788,6 +881,12 @@ class PlayerService:
|
|||||||
else:
|
else:
|
||||||
await self._stop_playback()
|
await self._stop_playback()
|
||||||
await self._broadcast_state()
|
await self._broadcast_state()
|
||||||
|
elif self.state.playlist_sounds:
|
||||||
|
# Current track was from play_next, go to playlist
|
||||||
|
await self.play(0)
|
||||||
|
else:
|
||||||
|
await self._stop_playback()
|
||||||
|
await self._broadcast_state()
|
||||||
|
|
||||||
async def _broadcast_state(self) -> None:
|
async def _broadcast_state(self) -> None:
|
||||||
"""Broadcast current player state via WebSocket."""
|
"""Broadcast current player state via WebSocket."""
|
||||||
|
|||||||
@@ -537,6 +537,7 @@ class TestPlayerEndpoints:
|
|||||||
"duration": 30000,
|
"duration": 30000,
|
||||||
"sounds": [],
|
"sounds": [],
|
||||||
},
|
},
|
||||||
|
"play_next_queue": [],
|
||||||
}
|
}
|
||||||
mock_player_service.get_state.return_value = mock_state
|
mock_player_service.get_state.return_value = mock_state
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user