This commit is contained in:
2026-10-27 12:03:22 +07:00
parent 0879a8946a
commit 0341bfac25
32 changed files with 4119 additions and 1350 deletions
+3 -2
View File
@@ -13,12 +13,13 @@ func playlistErrorStatus(err error, fallback string) error {
case errors.Is(err, playlist_service.ErrInvalidName),
errors.Is(err, playlist_service.ErrInvalidTrack):
return status.Error(codes.InvalidArgument, err.Error())
case errors.Is(err, playlist_service.ErrPlaylistNotFound),
case errors.Is(err, playlist_service.ErrGroupNotFound),
errors.Is(err, playlist_service.ErrPlaylistNotFound),
errors.Is(err, playlist_service.ErrTrackNotFound),
errors.Is(err, playlist_service.ErrGameNotFound),
errors.Is(err, playlist_service.ErrScenarioNotFound):
return status.Error(codes.NotFound, err.Error())
case errors.Is(err, playlist_service.ErrTrackOrderChanged):
case errors.Is(err, playlist_service.ErrTrackOrderChanged), errors.Is(err, playlist_service.ErrGroupOrderChanged):
return status.Error(codes.Aborted, err.Error())
case errors.Is(err, playlist_service.ErrPlaylistInUse):
return status.Error(codes.FailedPrecondition, err.Error())
+68
View File
@@ -0,0 +1,68 @@
package app
import (
"context"
proto "evening_detective_server/proto"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func (s *server) SavePlaylistGroup(ctx context.Context, req *proto.SavePlaylistGroupReq) (*proto.SavePlaylistGroupRsp, error) {
if !canManagePlaylists(ctx) {
return nil, status.Error(codes.PermissionDenied, "permission denied")
}
var mode string
switch req.GetEndMode() {
case proto.PlaylistGroupEndMode_PLAYLIST_GROUP_END_MODE_REPEAT:
mode = "repeat"
case proto.PlaylistGroupEndMode_PLAYLIST_GROUP_END_MODE_STOP:
mode = "stop"
case proto.PlaylistGroupEndMode_PLAYLIST_GROUP_END_MODE_NEXT:
mode = "next"
default:
return nil, status.Error(codes.InvalidArgument, "некорректный режим завершения группы")
}
item, err := s.playlistService.SaveGroup(ctx, int(req.GetPlaylistID()), int(req.GetGroupID()), req.GetName(), mode)
if err != nil {
return nil, playlistErrorStatus(err, "не удалось сохранить группу")
}
return &proto.SavePlaylistGroupRsp{Playlist: mapPlaylist(item)}, nil
}
func (s *server) DeletePlaylistGroup(ctx context.Context, req *proto.DeletePlaylistGroupReq) (*proto.DeletePlaylistGroupRsp, error) {
if !canManagePlaylists(ctx) {
return nil, status.Error(codes.PermissionDenied, "permission denied")
}
item, err := s.playlistService.DeleteGroup(ctx, int(req.GetPlaylistID()), int(req.GetGroupID()))
if err != nil {
return nil, playlistErrorStatus(err, "не удалось удалить группу")
}
return &proto.DeletePlaylistGroupRsp{Playlist: mapPlaylist(item)}, nil
}
func (s *server) ReorderPlaylistGroups(ctx context.Context, req *proto.ReorderPlaylistGroupsReq) (*proto.ReorderPlaylistGroupsRsp, error) {
if !canManagePlaylists(ctx) {
return nil, status.Error(codes.PermissionDenied, "permission denied")
}
ids := make([]int, len(req.GetGroupIDs()))
for i, id := range req.GetGroupIDs() {
ids[i] = int(id)
}
item, err := s.playlistService.ReorderGroups(ctx, int(req.GetPlaylistID()), ids)
if err != nil {
return nil, playlistErrorStatus(err, "не удалось изменить порядок групп")
}
return &proto.ReorderPlaylistGroupsRsp{Playlist: mapPlaylist(item)}, nil
}
func (s *server) SetPlaylistTrackGroup(ctx context.Context, req *proto.SetPlaylistTrackGroupReq) (*proto.SetPlaylistTrackGroupRsp, error) {
if !canManagePlaylists(ctx) {
return nil, status.Error(codes.PermissionDenied, "permission denied")
}
item, err := s.playlistService.SetTrackGroup(ctx, int(req.GetPlaylistID()), int(req.GetTrackID()), int(req.GetGroupID()))
if err != nil {
return nil, playlistErrorStatus(err, "не удалось изменить группу трека")
}
return &proto.SetPlaylistTrackGroupRsp{Playlist: mapPlaylist(item)}, nil
}
+53
View File
@@ -0,0 +1,53 @@
package app
import (
"context"
"testing"
proto "evening_detective_server/proto"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func TestPlaylistGroupsRequireManagementRights(t *testing.T) {
for _, tc := range []struct {
name string
call func(*server, context.Context) error
}{
{"save", func(s *server, ctx context.Context) error {
_, err := s.SavePlaylistGroup(ctx, &proto.SavePlaylistGroupReq{})
return err
}},
{"delete", func(s *server, ctx context.Context) error {
_, err := s.DeletePlaylistGroup(ctx, &proto.DeletePlaylistGroupReq{})
return err
}},
{"order", func(s *server, ctx context.Context) error {
_, err := s.ReorderPlaylistGroups(ctx, &proto.ReorderPlaylistGroupsReq{})
return err
}},
{"track", func(s *server, ctx context.Context) error {
_, err := s.SetPlaylistTrackGroup(ctx, &proto.SetPlaylistTrackGroupReq{})
return err
}},
} {
t.Run(tc.name, func(t *testing.T) {
for _, ctx := range []context.Context{context.Background(), ctxWithClaims("user")} {
if err := tc.call(&server{}, ctx); status.Code(err) != codes.PermissionDenied {
t.Fatal(err)
}
}
})
}
}
func TestSavePlaylistGroupRejectsUnknownMode(t *testing.T) {
for _, mode := range []proto.PlaylistGroupEndMode{-1, 99} {
t.Run(mode.String(), func(t *testing.T) {
_, err := (&server{}).SavePlaylistGroup(ctxWithClaims("organizer"), &proto.SavePlaylistGroupReq{EndMode: mode})
if status.Code(err) != codes.InvalidArgument {
t.Fatal(err)
}
})
}
}
+3
View File
@@ -19,7 +19,10 @@ func TestPlaylistOrderErrorCodes(t *testing.T) {
}{
{"invalid", playlist_service.ErrInvalidTrack, codes.InvalidArgument},
{"conflict", playlist_service.ErrTrackOrderChanged, codes.Aborted},
{"group conflict", playlist_service.ErrGroupOrderChanged, codes.Aborted},
{"missing", playlist_service.ErrPlaylistNotFound, codes.NotFound},
{"missing group", playlist_service.ErrGroupNotFound, codes.NotFound},
{"active group", playlist_service.ErrPlaylistInUse, codes.FailedPrecondition},
{"internal", errors.New("secret SQL"), codes.Internal},
}
for _, tc := range cases {
+1
View File
@@ -153,6 +153,7 @@ func (s *server) SetGameMusic(ctx context.Context, req *proto.SetGameMusicReq) (
req.GetVolume(),
req.GetLoop(),
int32(req.GetPlayback()),
int(req.GetGroupId()),
)
if err != nil {
return nil, playlistErrorStatus(err, "не удалось изменить музыку игры")
+16
View File
@@ -19,8 +19,12 @@ func mapPlaylist(value *playlist_service.Playlist) *proto.Playlist {
Name: track.Name,
Filename: track.Filename,
Position: int32(track.Position),
GroupId: int32(track.GroupID),
})
}
for _, group := range value.Groups {
result.Groups = append(result.Groups, &proto.PlaylistGroup{ID: int32(group.ID), Name: group.Name, Position: int32(group.Position), EndMode: mapGroupEndMode(group.EndMode)})
}
return result
}
@@ -31,6 +35,7 @@ func mapMusicState(value *playlist_service.MusicState) *proto.MusicState {
return &proto.MusicState{
Playlist: mapPlaylist(value.Playlist),
TrackIndex: int32(value.TrackIndex),
GroupId: int32(value.GroupID),
PositionMs: value.PositionMs,
Volume: value.Volume,
Loop: value.Loop,
@@ -38,3 +43,14 @@ func mapMusicState(value *playlist_service.MusicState) *proto.MusicState {
StateVersion: value.Version,
}
}
func mapGroupEndMode(mode string) proto.PlaylistGroupEndMode {
switch mode {
case "stop":
return proto.PlaylistGroupEndMode_PLAYLIST_GROUP_END_MODE_STOP
case "next":
return proto.PlaylistGroupEndMode_PLAYLIST_GROUP_END_MODE_NEXT
default:
return proto.PlaylistGroupEndMode_PLAYLIST_GROUP_END_MODE_REPEAT
}
}
+4 -15
View File
@@ -64,23 +64,12 @@ func (s *server) AdvanceScreenMusic(ctx context.Context, req *proto.AdvanceScree
if err != nil || session.GameID == nil {
return nil, status.Error(codes.FailedPrecondition, "экран не привязан к игре")
}
state, err := s.playlistService.GetGame(ctx, *session.GameID)
if err != nil || state.Playlist == nil || len(state.Playlist.Tracks) == 0 {
return &proto.AdvanceScreenMusicRsp{State: mapMusicState(state)}, nil
if !session.Sound {
return nil, status.Error(codes.FailedPrecondition, "экран не назначен для звука")
}
next := state.TrackIndex + 1
playback := state.Playback
if next >= len(state.Playlist.Tracks) {
if state.Loop {
next = 0
} else {
next = len(state.Playlist.Tracks) - 1
playback = 1
}
}
updated, err := s.playlistService.SetGame(ctx, *session.GameID, state.Playlist.ID, next, 0, state.Volume, state.Loop, playback)
updated, err := s.playlistService.Advance(ctx, *session.GameID, req.GetExpectedStateVersion())
if err != nil {
return nil, status.Error(codes.Internal, "не удалось переключить трек")
return nil, playlistErrorStatus(err, "не удалось переключить трек")
}
return &proto.AdvanceScreenMusicRsp{State: mapMusicState(updated)}, nil
}