This commit is contained in:
2026-10-18 05:28:57 +07:00
parent bfd6841d7d
commit e96199afba
18 changed files with 2331 additions and 1193 deletions
@@ -0,0 +1,35 @@
package playlist_service
import (
"context"
"errors"
"testing"
"evening_detective_server/internal/repos/playlists_repo"
)
func TestReorderRejectsInvalidIDsBeforeRepository(t *testing.T) {
cases := []struct {
name string
playlistID int
ids []int
}{
{"playlist", 0, []int{1}}, {"negative", 1, []int{-1}}, {"duplicate", 1, []int{1, 1}}, {"zero", 1, []int{0}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
_, err := New(nil, nil).Reorder(context.Background(), tc.playlistID, tc.ids)
if !errors.Is(err, ErrInvalidTrack) {
t.Fatal(err)
}
})
}
}
func TestOrderConflictMapping(t *testing.T) {
for _, tc := range []struct{ input, expected error }{{playlists_repo.ErrTrackOrderChanged, ErrTrackOrderChanged}} {
if !errors.Is(mapError(tc.input), tc.expected) {
t.Fatal("lost conflict")
}
}
}
+29 -7
View File
@@ -11,13 +11,14 @@ import (
)
var (
ErrInvalidName = errors.New("некорректное имя плейлиста")
ErrInvalidTrack = errors.New("некорректный трек")
ErrPlaylistNotFound = errors.New("плейлист не найден")
ErrPlaylistInUse = errors.New("плейлист используется")
ErrTrackNotFound = errors.New("трек не найден")
ErrGameNotFound = errors.New("игра не найдена")
ErrScenarioNotFound = errors.New("сценарий не найден")
ErrTrackOrderChanged = errors.New("состав треков изменился; обновите список")
ErrInvalidName = errors.New("некорректное имя плейлиста")
ErrInvalidTrack = errors.New("некорректный трек")
ErrPlaylistNotFound = errors.New("плейлист не найден")
ErrPlaylistInUse = errors.New("плейлист используется")
ErrTrackNotFound = errors.New("трек не найден")
ErrGameNotFound = errors.New("игра не найдена")
ErrScenarioNotFound = errors.New("сценарий не найден")
)
// Service implements playlist and game music use cases.
@@ -150,6 +151,8 @@ func (s *Service) GetGame(ctx context.Context, gameID int) (*MusicState, error)
func mapError(err error) error {
switch {
case errors.Is(err, playlists_repo.ErrTrackOrderChanged):
return ErrTrackOrderChanged
case errors.Is(err, playlists_repo.ErrPlaylistNotFound):
return ErrPlaylistNotFound
case errors.Is(err, playlists_repo.ErrPlaylistInUse):
@@ -164,3 +167,22 @@ func mapError(err error) error {
return err
}
}
// Reorder validates a complete track order and preserves live playback.
func (s *Service) Reorder(ctx context.Context, playlistID int, trackIDs []int) (*Playlist, error) {
if playlistID <= 0 {
return nil, ErrInvalidTrack
}
seen := make(map[int]bool, len(trackIDs))
for _, id := range trackIDs {
if id <= 0 || seen[id] {
return nil, ErrInvalidTrack
}
seen[id] = true
}
item, err := s.repo.Reorder(ctx, playlistID, trackIDs)
if err != nil {
return nil, mapError(err)
}
return mapPlaylist(item), nil
}
@@ -100,15 +100,16 @@ func (s *ScreenPlanService) SetStep(ctx context.Context, gameID, currentStep int
}
// ApplyCurrent applies the current step when a screen joins a game.
func (s *ScreenPlanService) ApplyCurrent(ctx context.Context, gameID int) error {
func (s *ScreenPlanService) ApplyCurrent(ctx context.Context, gameID, screenID int) error {
plan, err := s.Get(ctx, gameID)
if errors.Is(err, ErrPlanNotFound) {
return nil
return s.screenService.InitializeScreen(ctx, screenID, gameID, 1, "", "Перерыв", 600)
}
if err != nil {
return err
}
return s.applyStep(ctx, gameID, plan)
step := plan.Steps[plan.CurrentStep]
return s.screenService.InitializeScreen(ctx, screenID, gameID, step.Mode, step.Text, step.TimerLabel, step.TimerDurationSeconds)
}
func normalizePlan(steps []Step, currentStep int) ([]Step, error) {
+8 -2
View File
@@ -73,8 +73,9 @@ func (s *ScreenService) SetCommand(ctx context.Context, gameID int, command int3
}
// Bind attaches a screen session to a game.
func (s *ScreenService) Bind(ctx context.Context, id, gameID int, tokenHash string) error {
return mapError(s.repo.Bind(ctx, id, gameID, tokenHash))
func (s *ScreenService) Bind(ctx context.Context, id, gameID int, tokenHash string, initializeAudio bool) (bool, error) {
isNewBinding, err := s.repo.BindWithAudio(ctx, id, gameID, tokenHash, initializeAudio)
return isNewBinding, mapError(err)
}
// Unbind detaches a screen session from its game.
@@ -93,3 +94,8 @@ func mapError(err error) error {
}
return err
}
// InitializeScreen joins the current display without resetting other screens.
func (s *ScreenService) InitializeScreen(ctx context.Context, id, gameID int, mode int32, text, label string, duration int64) error {
return s.repo.InitializeScreen(ctx, id, gameID, mode, text, label, duration)
}