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
+102 -2
View File
@@ -342,6 +342,49 @@ service EveningDetectiveServer {
}; };
} }
rpc SavePlaylistGroup(SavePlaylistGroupReq) returns (SavePlaylistGroupRsp) {
option (google.api.http) = {
post: "/api/playlists/{playlistID}/groups"
body: "*"
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags : "Музыка";
summary: "Сохранить группу плейлиста";
};
}
rpc DeletePlaylistGroup(DeletePlaylistGroupReq) returns (DeletePlaylistGroupRsp) {
option (google.api.http) = {
delete: "/api/playlists/{playlistID}/groups/{groupID}"
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags : "Музыка";
summary: "Удалить группу, сохранив треки";
};
}
rpc ReorderPlaylistGroups(ReorderPlaylistGroupsReq) returns (ReorderPlaylistGroupsRsp) {
option (google.api.http) = {
put : "/api/playlists/{playlistID}/groups/order"
body: "*"
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags : "Музыка";
summary: "Изменить порядок групп плейлиста";
};
}
rpc SetPlaylistTrackGroup(SetPlaylistTrackGroupReq) returns (SetPlaylistTrackGroupRsp) {
option (google.api.http) = {
put : "/api/playlists/{playlistID}/tracks/{trackID}/group"
body: "*"
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags : "Музыка";
summary: "Назначить треку группу";
};
}
rpc GetPlaylists(GetPlaylistsReq) returns (GetPlaylistsRsp) { rpc GetPlaylists(GetPlaylistsReq) returns (GetPlaylistsRsp) {
option (google.api.http) = { get: "/api/playlists" }; option (google.api.http) = { get: "/api/playlists" };
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
@@ -1400,17 +1443,71 @@ message DownloadFileReq {
string filename = 1; string filename = 1;
} }
enum PlaylistGroupEndMode {
PLAYLIST_GROUP_END_MODE_REPEAT = 0;
PLAYLIST_GROUP_END_MODE_STOP = 1;
PLAYLIST_GROUP_END_MODE_NEXT = 2;
}
message PlaylistGroup {
int32 ID = 1;
string name = 2;
int32 position = 3;
PlaylistGroupEndMode end_mode = 4;
}
message SavePlaylistGroupReq {
int32 playlistID = 1;
int32 groupID = 2;
string name = 3;
PlaylistGroupEndMode end_mode = 4;
}
message SavePlaylistGroupRsp {
Playlist playlist = 1;
}
message DeletePlaylistGroupReq {
int32 playlistID = 1;
int32 groupID = 2;
}
message DeletePlaylistGroupRsp {
Playlist playlist = 1;
}
message ReorderPlaylistGroupsReq {
int32 playlistID = 1;
repeated int32 groupIDs = 2;
}
message ReorderPlaylistGroupsRsp {
Playlist playlist = 1;
}
message SetPlaylistTrackGroupReq {
int32 playlistID = 1;
int32 trackID = 2;
int32 groupID = 3;
}
message SetPlaylistTrackGroupRsp {
Playlist playlist = 1;
}
message PlaylistTrack { message PlaylistTrack {
int32 ID = 1; int32 ID = 1;
string name = 2; string name = 2;
string filename = 3; string filename = 3;
int32 position = 4; int32 position = 4;
int32 group_id = 5;
} }
message Playlist { message Playlist {
int32 ID = 1; int32 ID = 1;
string name = 2; string name = 2;
repeated PlaylistTrack tracks = 3; repeated PlaylistTrack tracks = 3;
repeated PlaylistGroup groups = 4;
} }
message GetPlaylistsReq {} message GetPlaylistsReq {}
@@ -1885,6 +1982,7 @@ message MusicState {
bool loop = 5; bool loop = 5;
MusicPlaybackState playback = 6; MusicPlaybackState playback = 6;
int64 state_version = 7; int64 state_version = 7;
int32 group_id = 8;
} }
message GetGameMusicReq { message GetGameMusicReq {
@@ -1906,6 +2004,7 @@ message SetGameMusicReq {
int32 volume = 5; int32 volume = 5;
bool loop = 6; bool loop = 6;
MusicPlaybackState playback = 7; MusicPlaybackState playback = 7;
int32 group_id = 8;
} }
message SetGameMusicRsp { message SetGameMusicRsp {
@@ -1913,8 +2012,9 @@ message SetGameMusicRsp {
} }
message AdvanceScreenMusicReq { message AdvanceScreenMusicReq {
string screenID = 1; string screenID = 1;
string accessToken = 2; string accessToken = 2;
string expected_state_version = 3;
} }
message AdvanceScreenMusicRsp { message AdvanceScreenMusicRsp {
@@ -2014,6 +2014,125 @@
] ]
} }
}, },
"/api/playlists/{playlistID}/groups": {
"post": {
"summary": "Сохранить группу плейлиста",
"operationId": "EveningDetectiveServer_SavePlaylistGroup",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/evening_detective_serverSavePlaylistGroupRsp"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "playlistID",
"in": "path",
"required": true,
"type": "integer",
"format": "int32"
},
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/EveningDetectiveServerSavePlaylistGroupBody"
}
}
],
"tags": [
"Музыка"
]
}
},
"/api/playlists/{playlistID}/groups/order": {
"put": {
"summary": "Изменить порядок групп плейлиста",
"operationId": "EveningDetectiveServer_ReorderPlaylistGroups",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/evening_detective_serverReorderPlaylistGroupsRsp"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "playlistID",
"in": "path",
"required": true,
"type": "integer",
"format": "int32"
},
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/EveningDetectiveServerReorderPlaylistGroupsBody"
}
}
],
"tags": [
"Музыка"
]
}
},
"/api/playlists/{playlistID}/groups/{groupID}": {
"delete": {
"summary": "Удалить группу, сохранив треки",
"operationId": "EveningDetectiveServer_DeletePlaylistGroup",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/evening_detective_serverDeletePlaylistGroupRsp"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "playlistID",
"in": "path",
"required": true,
"type": "integer",
"format": "int32"
},
{
"name": "groupID",
"in": "path",
"required": true,
"type": "integer",
"format": "int32"
}
],
"tags": [
"Музыка"
]
}
},
"/api/playlists/{playlistID}/tracks": { "/api/playlists/{playlistID}/tracks": {
"post": { "post": {
"summary": "Загрузить MP3 в плейлист", "summary": "Загрузить MP3 в плейлист",
@@ -2133,6 +2252,53 @@
] ]
} }
}, },
"/api/playlists/{playlistID}/tracks/{trackID}/group": {
"put": {
"summary": "Назначить треку группу",
"operationId": "EveningDetectiveServer_SetPlaylistTrackGroup",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/evening_detective_serverSetPlaylistTrackGroupRsp"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "playlistID",
"in": "path",
"required": true,
"type": "integer",
"format": "int32"
},
{
"name": "trackID",
"in": "path",
"required": true,
"type": "integer",
"format": "int32"
},
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/EveningDetectiveServerSetPlaylistTrackGroupBody"
}
}
],
"tags": [
"Музыка"
]
}
},
"/api/privacy": { "/api/privacy": {
"get": { "get": {
"summary": "Получить текст Политики конфиденциальности", "summary": "Получить текст Политики конфиденциальности",
@@ -4322,6 +4488,18 @@
"EveningDetectiveServerPublicScenarioBody": { "EveningDetectiveServerPublicScenarioBody": {
"type": "object" "type": "object"
}, },
"EveningDetectiveServerReorderPlaylistGroupsBody": {
"type": "object",
"properties": {
"groupIDs": {
"type": "array",
"items": {
"type": "integer",
"format": "int32"
}
}
}
},
"EveningDetectiveServerReorderPlaylistTracksBody": { "EveningDetectiveServerReorderPlaylistTracksBody": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -4346,6 +4524,21 @@
} }
} }
}, },
"EveningDetectiveServerSavePlaylistGroupBody": {
"type": "object",
"properties": {
"groupID": {
"type": "integer",
"format": "int32"
},
"name": {
"type": "string"
},
"endMode": {
"$ref": "#/definitions/evening_detective_serverPlaylistGroupEndMode"
}
}
},
"EveningDetectiveServerSaveScreenPlanBody": { "EveningDetectiveServerSaveScreenPlanBody": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -4402,6 +4595,19 @@
}, },
"playback": { "playback": {
"$ref": "#/definitions/evening_detective_serverMusicPlaybackState" "$ref": "#/definitions/evening_detective_serverMusicPlaybackState"
},
"groupId": {
"type": "integer",
"format": "int32"
}
}
},
"EveningDetectiveServerSetPlaylistTrackGroupBody": {
"type": "object",
"properties": {
"groupID": {
"type": "integer",
"format": "int32"
} }
} }
}, },
@@ -4795,6 +5001,9 @@
}, },
"accessToken": { "accessToken": {
"type": "string" "type": "string"
},
"expectedStateVersion": {
"type": "string"
} }
} }
}, },
@@ -4955,6 +5164,14 @@
"evening_detective_serverDeleteLastTeamActionRsp": { "evening_detective_serverDeleteLastTeamActionRsp": {
"type": "object" "type": "object"
}, },
"evening_detective_serverDeletePlaylistGroupRsp": {
"type": "object",
"properties": {
"playlist": {
"$ref": "#/definitions/evening_detective_serverPlaylist"
}
}
},
"evening_detective_serverDeletePlaylistRsp": { "evening_detective_serverDeletePlaylistRsp": {
"type": "object" "type": "object"
}, },
@@ -5837,6 +6054,10 @@
"stateVersion": { "stateVersion": {
"type": "string", "type": "string",
"format": "int64" "format": "int64"
},
"groupId": {
"type": "integer",
"format": "int32"
} }
} }
}, },
@@ -5937,9 +6158,44 @@
"type": "object", "type": "object",
"$ref": "#/definitions/evening_detective_serverPlaylistTrack" "$ref": "#/definitions/evening_detective_serverPlaylistTrack"
} }
},
"groups": {
"type": "array",
"items": {
"type": "object",
"$ref": "#/definitions/evening_detective_serverPlaylistGroup"
}
} }
} }
}, },
"evening_detective_serverPlaylistGroup": {
"type": "object",
"properties": {
"ID": {
"type": "integer",
"format": "int32"
},
"name": {
"type": "string"
},
"position": {
"type": "integer",
"format": "int32"
},
"endMode": {
"$ref": "#/definitions/evening_detective_serverPlaylistGroupEndMode"
}
}
},
"evening_detective_serverPlaylistGroupEndMode": {
"type": "string",
"enum": [
"PLAYLIST_GROUP_END_MODE_REPEAT",
"PLAYLIST_GROUP_END_MODE_STOP",
"PLAYLIST_GROUP_END_MODE_NEXT"
],
"default": "PLAYLIST_GROUP_END_MODE_REPEAT"
},
"evening_detective_serverPlaylistTrack": { "evening_detective_serverPlaylistTrack": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -5956,6 +6212,10 @@
"position": { "position": {
"type": "integer", "type": "integer",
"format": "int32" "format": "int32"
},
"groupId": {
"type": "integer",
"format": "int32"
} }
} }
}, },
@@ -6013,6 +6273,14 @@
} }
} }
}, },
"evening_detective_serverReorderPlaylistGroupsRsp": {
"type": "object",
"properties": {
"playlist": {
"$ref": "#/definitions/evening_detective_serverPlaylist"
}
}
},
"evening_detective_serverReorderPlaylistTracksRsp": { "evening_detective_serverReorderPlaylistTracksRsp": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -6039,6 +6307,14 @@
}, },
"description": "Вопрос в шапке итоговой таблицы: правильный ответ (поле answer сообщения\nQuestion) командам/таблице не передаётся — он виден только в панели оценки." "description": "Вопрос в шапке итоговой таблицы: правильный ответ (поле answer сообщения\nQuestion) командам/таблице не передаётся — он виден только в панели оценки."
}, },
"evening_detective_serverSavePlaylistGroupRsp": {
"type": "object",
"properties": {
"playlist": {
"$ref": "#/definitions/evening_detective_serverPlaylist"
}
}
},
"evening_detective_serverSaveScreenPlanRsp": { "evening_detective_serverSaveScreenPlanRsp": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -6296,6 +6572,14 @@
} }
} }
}, },
"evening_detective_serverSetPlaylistTrackGroupRsp": {
"type": "object",
"properties": {
"playlist": {
"$ref": "#/definitions/evening_detective_serverPlaylist"
}
}
},
"evening_detective_serverSetScenarioPlaylistRsp": { "evening_detective_serverSetScenarioPlaylistRsp": {
"type": "object" "type": "object"
}, },
+3 -2
View File
@@ -13,12 +13,13 @@ func playlistErrorStatus(err error, fallback string) error {
case errors.Is(err, playlist_service.ErrInvalidName), case errors.Is(err, playlist_service.ErrInvalidName),
errors.Is(err, playlist_service.ErrInvalidTrack): errors.Is(err, playlist_service.ErrInvalidTrack):
return status.Error(codes.InvalidArgument, err.Error()) 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.ErrTrackNotFound),
errors.Is(err, playlist_service.ErrGameNotFound), errors.Is(err, playlist_service.ErrGameNotFound),
errors.Is(err, playlist_service.ErrScenarioNotFound): errors.Is(err, playlist_service.ErrScenarioNotFound):
return status.Error(codes.NotFound, err.Error()) 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()) return status.Error(codes.Aborted, err.Error())
case errors.Is(err, playlist_service.ErrPlaylistInUse): case errors.Is(err, playlist_service.ErrPlaylistInUse):
return status.Error(codes.FailedPrecondition, err.Error()) 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}, {"invalid", playlist_service.ErrInvalidTrack, codes.InvalidArgument},
{"conflict", playlist_service.ErrTrackOrderChanged, codes.Aborted}, {"conflict", playlist_service.ErrTrackOrderChanged, codes.Aborted},
{"group conflict", playlist_service.ErrGroupOrderChanged, codes.Aborted},
{"missing", playlist_service.ErrPlaylistNotFound, codes.NotFound}, {"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}, {"internal", errors.New("secret SQL"), codes.Internal},
} }
for _, tc := range cases { for _, tc := range cases {
+1
View File
@@ -153,6 +153,7 @@ func (s *server) SetGameMusic(ctx context.Context, req *proto.SetGameMusicReq) (
req.GetVolume(), req.GetVolume(),
req.GetLoop(), req.GetLoop(),
int32(req.GetPlayback()), int32(req.GetPlayback()),
int(req.GetGroupId()),
) )
if err != nil { if err != nil {
return nil, playlistErrorStatus(err, "не удалось изменить музыку игры") return nil, playlistErrorStatus(err, "не удалось изменить музыку игры")
+16
View File
@@ -19,8 +19,12 @@ func mapPlaylist(value *playlist_service.Playlist) *proto.Playlist {
Name: track.Name, Name: track.Name,
Filename: track.Filename, Filename: track.Filename,
Position: int32(track.Position), 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 return result
} }
@@ -31,6 +35,7 @@ func mapMusicState(value *playlist_service.MusicState) *proto.MusicState {
return &proto.MusicState{ return &proto.MusicState{
Playlist: mapPlaylist(value.Playlist), Playlist: mapPlaylist(value.Playlist),
TrackIndex: int32(value.TrackIndex), TrackIndex: int32(value.TrackIndex),
GroupId: int32(value.GroupID),
PositionMs: value.PositionMs, PositionMs: value.PositionMs,
Volume: value.Volume, Volume: value.Volume,
Loop: value.Loop, Loop: value.Loop,
@@ -38,3 +43,14 @@ func mapMusicState(value *playlist_service.MusicState) *proto.MusicState {
StateVersion: value.Version, 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 { if err != nil || session.GameID == nil {
return nil, status.Error(codes.FailedPrecondition, "экран не привязан к игре") return nil, status.Error(codes.FailedPrecondition, "экран не привязан к игре")
} }
state, err := s.playlistService.GetGame(ctx, *session.GameID) if !session.Sound {
if err != nil || state.Playlist == nil || len(state.Playlist.Tracks) == 0 { return nil, status.Error(codes.FailedPrecondition, "экран не назначен для звука")
return &proto.AdvanceScreenMusicRsp{State: mapMusicState(state)}, nil
} }
next := state.TrackIndex + 1 updated, err := s.playlistService.Advance(ctx, *session.GameID, req.GetExpectedStateVersion())
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)
if err != nil { if err != nil {
return nil, status.Error(codes.Internal, "не удалось переключить трек") return nil, playlistErrorStatus(err, "не удалось переключить трек")
} }
return &proto.AdvanceScreenMusicRsp{State: mapMusicState(updated)}, nil return &proto.AdvanceScreenMusicRsp{State: mapMusicState(updated)}, nil
} }
+26 -2
View File
@@ -36,9 +36,14 @@ func TestExportSnapshotAndEditableGame(t *testing.T) {
requireNoError(t, err) requireNoError(t, err)
selected, err := music.Create(ctx, "Selected by screen") selected, err := music.Create(ctx, "Selected by screen")
requireNoError(t, err) requireNoError(t, err)
requireNoError(t, music.AddTrack(ctx, selected.ID, "Grouped", "grouped.mp3"))
selected, err = music.SaveGroup(ctx, selected.ID, 0, "Final", "stop")
requireNoError(t, err)
selected, err = music.SetTrackGroup(ctx, selected.ID, selected.Tracks[0].ID, selected.Groups[0].ID)
requireNoError(t, err)
requireNoError(t, music.SetScenario(ctx, f.scenarioID, base.ID)) requireNoError(t, music.SetScenario(ctx, f.scenarioID, base.ID))
requireNoError(t, music.SetGame(ctx, f.gameID, assigned.ID)) requireNoError(t, music.SetGame(ctx, f.gameID, assigned.ID))
_, err = music.SetMusic(ctx, f.gameID, selected.ID, 0, 0, 100, false, 1) _, err = music.SetMusic(ctx, f.gameID, selected.ID, 0, 0, 100, false, 1, 0)
requireNoError(t, err) requireNoError(t, err)
ticketRepo := tickets_repo.New(f.pool) ticketRepo := tickets_repo.New(f.pool)
activeHash := sha256.Sum256([]byte("active-ticket")) activeHash := sha256.Sum256([]byte("active-ticket"))
@@ -86,6 +91,25 @@ func TestExportSnapshotAndEditableGame(t *testing.T) {
} }
} }
requireNoError(t, json.Unmarshal(attempt.Snapshot, &snapshot)) requireNoError(t, json.Unmarshal(attempt.Snapshot, &snapshot))
var groupedSnapshot struct {
Playlists []struct {
ID int `json:"id"`
Groups []struct {
ID int `json:"id"`
EndMode string `json:"end_mode"`
} `json:"groups"`
Tracks []struct {
GroupID int `json:"group_id"`
} `json:"tracks"`
} `json:"playlists"`
}
requireNoError(t, json.Unmarshal(attempt.Snapshot, &groupedSnapshot))
for _, p := range groupedSnapshot.Playlists {
if p.ID == selected.ID && (len(p.Groups) != 1 || p.Groups[0].EndMode != "stop" || len(p.Tracks) != 1 || p.Tracks[0].GroupID != p.Groups[0].ID) {
t.Fatal("export lost group metadata")
}
}
if snapshot.Game.PlaylistID != selected.ID || len(snapshot.Playlists) != 2 || len(snapshot.Tickets) != 2 || if snapshot.Game.PlaylistID != selected.ID || len(snapshot.Playlists) != 2 || len(snapshot.Tickets) != 2 ||
!bytes.Equal(snapshot.Tickets[0].TokenHash, activeHash[:]) || snapshot.Teams[0].Password != "original-qr" { !bytes.Equal(snapshot.Tickets[0].TokenHash, activeHash[:]) || snapshot.Teams[0].Password != "original-qr" {
t.Fatalf("snapshot lost effective music, ticket or QR: %+v", snapshot) t.Fatalf("snapshot lost effective music, ticket or QR: %+v", snapshot)
@@ -99,7 +123,7 @@ func TestExportSnapshotAndEditableGame(t *testing.T) {
requireNoError(t, err) requireNoError(t, err)
_, err = ticketRepo.Accept(ctx, activeHash[:], f.userID) _, err = ticketRepo.Accept(ctx, activeHash[:], f.userID)
requireNoError(t, err) requireNoError(t, err)
_, err = music.SetMusic(ctx, f.gameID, base.ID, 0, 0, 100, false, 1) _, err = music.SetMusic(ctx, f.gameID, base.ID, 0, 0, 100, false, 1, 0)
requireNoError(t, err) requireNoError(t, err)
_, err = plans.Save(ctx, f.gameID, []game_screen_plans_repo.Step{{Mode: 2}}, 0) _, err = plans.Save(ctx, f.gameID, []game_screen_plans_repo.Step{{Mode: 2}}, 0)
requireNoError(t, err) requireNoError(t, err)
+8 -2
View File
@@ -48,9 +48,15 @@ func captureSnapshot(ctx context.Context, tx pgx.Tx, gameID int) ([]byte, error)
), '[]'::jsonb), ), '[]'::jsonb),
'playlists', COALESCE(( 'playlists', COALESCE((
SELECT jsonb_agg(jsonb_build_object( SELECT jsonb_agg(jsonb_build_object(
'id', p.id, 'name', p.name, 'tracks', COALESCE(( 'id', p.id, 'name', p.name, 'groups', COALESCE((
SELECT jsonb_agg(jsonb_build_object(
'id', pg.id, 'name', pg.name, 'position', pg.position, 'end_mode', pg.end_mode
) ORDER BY pg.position, pg.id)
FROM playlist_groups pg
WHERE pg.playlist_id = p.id
), '[]'::jsonb), 'tracks', COALESCE((
SELECT jsonb_agg(jsonb_build_object( SELECT jsonb_agg(jsonb_build_object(
'id', tr.id, 'name', tr.name, 'filename', tr.filename, 'position', tr.position 'id', tr.id, 'name', tr.name, 'filename', tr.filename, 'position', tr.position, 'group_id', COALESCE(tr.group_id, 0)
) ORDER BY tr.position, tr.id) ) ORDER BY tr.position, tr.id)
FROM playlist_tracks tr FROM playlist_tracks tr
WHERE tr.playlist_id = p.id WHERE tr.playlist_id = p.id
@@ -0,0 +1,59 @@
package game_imports_repo
import (
"context"
"encoding/json"
"testing"
"time"
"evening_detective_server/internal/modules/storytelling"
"evening_detective_server/internal/repos/playlists_repo"
"evening_detective_server/internal/services/game_package_service"
)
func TestImportPreservesPlaylistGroupsAndLegacyTracks(t *testing.T) {
for _, grouped := range []bool{false, true} {
t.Run(map[bool]string{false: "legacy", true: "groups"}[grouped], func(t *testing.T) {
repo, actor := repoFixture(t)
ctx := context.Background()
now := time.Now().UTC()
playlistID := 900
data := game_package_service.Data{
Game: game_package_service.Game{ID: 100, Name: "Game", StartAt: now, Status: "draft", Format: "offline", ScenarioID: 200, PlaylistID: &playlistID},
Scenario: game_package_service.Scenario{ID: 200, Name: "Story", Story: &storytelling.Story{}, Status: "draft", CreatedAt: now, UpdatedAt: now},
Playlists: []game_package_service.Playlist{{ID: playlistID, Name: "Music", Tracks: []game_package_service.Track{{ID: 901, Name: "One", Filename: "one.mp3", Position: 3}, {ID: 902, Name: "Two", Filename: "two.mp3", Position: 4}}}},
}
if grouped {
data.Playlists[0].Groups = []game_package_service.PlaylistGroup{{ID: 800, Name: "Second", Position: 0, EndMode: "stop"}, {ID: 801, Name: "First", Position: 1, EndMode: "next"}, {ID: 802, Name: "Empty", Position: 2, EndMode: "repeat"}}
data.Playlists[0].Tracks[0].GroupID = 801
data.Playlists[0].Tracks[1].GroupID = 800
}
raw, err := json.Marshal(data)
if err != nil {
t.Fatal(err)
}
attempt, err := repo.Start(ctx, &Attempt{ID: "group-import", PackageID: "group-package", SourceServer: "https://online.example", GameID: 100, SHA256: "digest", InitiatorID: actor, Manifest: []byte(`{}`), OriginalData: raw, ObjectKeys: []string{}})
if err != nil {
t.Fatal(err)
}
if err := repo.Complete(ctx, attempt.ID, raw); err != nil {
t.Fatal(err)
}
items, err := playlists_repo.New(repo.pool).List(ctx)
if err != nil {
t.Fatal(err)
}
if len(items) != 1 || len(items[0].Tracks) != 2 {
t.Fatalf("lost playlist: %+v", items)
}
p := items[0]
if grouped {
if len(p.Groups) != 3 || p.Groups[0].Name != "Second" || p.Groups[1].EndMode != "next" || p.Groups[2].EndMode != "repeat" || p.Tracks[0].GroupID != p.Groups[1].ID || p.Tracks[1].GroupID != p.Groups[0].ID {
t.Fatalf("lost group mappings: %+v", p)
}
} else if len(p.Groups) != 0 || p.Tracks[0].GroupID != 0 {
t.Fatal("legacy tracks gained groups")
}
})
}
}
+45 -7
View File
@@ -178,8 +178,9 @@ func insertPlaylists(ctx context.Context, tx pgx.Tx, data []byte, mappings *Mapp
`SELECT `SELECT
id, id,
name, name,
tracks tracks,
FROM jsonb_to_recordset(COALESCE(NULLIF($1::jsonb->'playlists', 'null'::jsonb), '[]'::jsonb)) AS p(id integer, name text, tracks jsonb) groups
FROM jsonb_to_recordset(COALESCE(NULLIF($1::jsonb->'playlists', 'null'::jsonb), '[]'::jsonb)) AS p(id integer, name text, tracks jsonb, groups jsonb)
ORDER BY id`, ORDER BY id`,
data, data,
) )
@@ -191,11 +192,12 @@ func insertPlaylists(ctx context.Context, tx pgx.Tx, data []byte, mappings *Mapp
id int id int
name string name string
tracks []byte tracks []byte
groups []byte
} }
var playlists []playlist var playlists []playlist
for rows.Next() { for rows.Next() {
var p playlist var p playlist
if err := rows.Scan(&p.id, &p.name, &p.tracks); err != nil { if err := rows.Scan(&p.id, &p.name, &p.tracks, &p.groups); err != nil {
return err return err
} }
playlists = append(playlists, p) playlists = append(playlists, p)
@@ -218,6 +220,39 @@ func insertPlaylists(ctx context.Context, tx pgx.Tx, data []byte, mappings *Mapp
return err return err
} }
mappings.Playlists[p.id] = localID mappings.Playlists[p.id] = localID
var groupMap []byte
err = tx.QueryRow(
ctx,
`WITH source AS MATERIALIZED (
SELECT
id AS source_id,
nextval(pg_get_serial_sequence('playlist_groups', 'id'))::integer AS local_id,
name,
position,
end_mode
FROM jsonb_to_recordset(COALESCE(NULLIF($1::jsonb, 'null'::jsonb), '[]'::jsonb)) AS g(id integer, name text, position integer, end_mode text)
), inserted AS (
INSERT INTO playlist_groups (id, playlist_id, name, position, end_mode)
SELECT
local_id,
$2,
name,
position,
end_mode
FROM source
RETURNING
id
)
SELECT
COALESCE(jsonb_object_agg(s.source_id, s.local_id), '{}'::jsonb)
FROM source s
JOIN inserted i ON i.id = s.local_id`,
p.groups,
localID,
).Scan(&groupMap)
if err != nil {
return err
}
var trackMap []byte var trackMap []byte
err = tx.QueryRow( err = tx.QueryRow(
ctx, ctx,
@@ -227,16 +262,18 @@ func insertPlaylists(ctx context.Context, tx pgx.Tx, data []byte, mappings *Mapp
nextval(pg_get_serial_sequence('playlist_tracks', 'id'))::integer AS local_id, nextval(pg_get_serial_sequence('playlist_tracks', 'id'))::integer AS local_id,
name, name,
filename, filename,
position position,
FROM jsonb_to_recordset(COALESCE(NULLIF($1::jsonb, 'null'::jsonb), '[]'::jsonb)) AS t(id integer, name text, filename text, position integer) group_id
FROM jsonb_to_recordset(COALESCE(NULLIF($1::jsonb, 'null'::jsonb), '[]'::jsonb)) AS t(id integer, name text, filename text, position integer, group_id integer)
), inserted AS ( ), inserted AS (
INSERT INTO playlist_tracks (id, playlist_id, name, filename, position) INSERT INTO playlist_tracks (id, playlist_id, name, filename, position, group_id)
SELECT SELECT
local_id, local_id,
$2, $2,
name, name,
filename, filename,
position position,
($3::jsonb->>group_id::text)::integer
FROM source FROM source
RETURNING RETURNING
id id
@@ -247,6 +284,7 @@ func insertPlaylists(ctx context.Context, tx pgx.Tx, data []byte, mappings *Mapp
JOIN inserted i ON i.id = s.local_id`, JOIN inserted i ON i.id = s.local_id`,
p.tracks, p.tracks,
localID, localID,
groupMap,
).Scan(&trackMap) ).Scan(&trackMap)
if err != nil { if err != nil {
return err return err
+288
View File
@@ -0,0 +1,288 @@
package playlists_repo
import (
"context"
"errors"
"slices"
"github.com/jackc/pgx/v5"
)
// groupInUse runs under the playlist lock shared with playback updates.
func groupInUse(ctx context.Context, tx pgx.Tx, playlistID, groupID int) error {
var used bool
err := tx.QueryRow(
ctx,
`SELECT EXISTS (
SELECT 1
FROM game_music_states ms
JOIN games g ON g.id = ms.game_id
JOIN scenarios s ON s.id = g.scenario_id
WHERE COALESCE(ms.playlist_id, g.playlist_id, s.playlist_id) = $1
AND ms.playback IN (2, 3)
AND ($2 = 0 OR ms.group_id IS NULL OR ms.group_id = $2)
)`,
playlistID,
groupID,
).Scan(&used)
if err != nil {
return err
}
if used {
return ErrPlaylistInUse
}
return nil
}
func readGroup(ctx context.Context, tx pgx.Tx, playlistID, groupID int) (*Group, error) {
var group Group
err := tx.QueryRow(
ctx,
`SELECT
id,
name,
position,
end_mode
FROM playlist_groups
WHERE playlist_id = $1 AND id = $2`,
playlistID,
groupID,
).Scan(&group.ID, &group.Name, &group.Position, &group.EndMode)
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrGroupNotFound
}
return &group, err
}
// SaveGroup creates a group or changes its name and completion mode.
func (r *Repo) SaveGroup(ctx context.Context, playlistID, groupID int, name, mode string) (*Playlist, error) {
tx, err := r.pool.Begin(ctx)
if err != nil {
return nil, err
}
defer tx.Rollback(ctx)
if err := lockPlaylist(ctx, tx, playlistID); err != nil {
return nil, err
}
if groupID == 0 {
_, err = tx.Exec(
ctx,
`INSERT INTO playlist_groups (playlist_id, name, position, end_mode)
SELECT
$1,
$2,
COALESCE(MAX(position), -1) + 1,
$3
FROM playlist_groups
WHERE playlist_id = $1`,
playlistID,
name,
mode,
)
} else {
group, readErr := readGroup(ctx, tx, playlistID, groupID)
if readErr != nil {
return nil, readErr
}
if mode != group.EndMode {
if err := groupInUse(ctx, tx, playlistID, groupID); err != nil {
return nil, err
}
}
_, err = tx.Exec(
ctx,
`UPDATE playlist_groups
SET name = $3, end_mode = $4
WHERE playlist_id = $1 AND id = $2`,
playlistID,
groupID,
name,
mode,
)
}
if err != nil {
return nil, err
}
if err := tx.Commit(ctx); err != nil {
return nil, err
}
return r.Get(ctx, playlistID)
}
// DeleteGroup keeps its tracks and clears stopped selections before deleting it.
func (r *Repo) DeleteGroup(ctx context.Context, playlistID, groupID int) (*Playlist, error) {
tx, err := r.pool.Begin(ctx)
if err != nil {
return nil, err
}
defer tx.Rollback(ctx)
if err := lockPlaylist(ctx, tx, playlistID); err != nil {
return nil, err
}
if _, err := readGroup(ctx, tx, playlistID, groupID); err != nil {
return nil, err
}
if err := groupInUse(ctx, tx, playlistID, groupID); err != nil {
return nil, err
}
_, err = tx.Exec(
ctx,
`UPDATE playlist_tracks
SET group_id = NULL
WHERE playlist_id = $1 AND group_id = $2`,
playlistID,
groupID,
)
if err != nil {
return nil, err
}
_, err = tx.Exec(
ctx,
`UPDATE game_music_states
SET group_id = NULL, state_version = state_version + 1
WHERE playlist_id = $1 AND group_id = $2`,
playlistID,
groupID,
)
if err != nil {
return nil, err
}
_, err = tx.Exec(
ctx,
`DELETE FROM playlist_groups
WHERE playlist_id = $1 AND id = $2`,
playlistID,
groupID,
)
if err != nil {
return nil, err
}
if err := tx.Commit(ctx); err != nil {
return nil, err
}
return r.Get(ctx, playlistID)
}
// ReorderGroups requires a complete group order to detect stale editors.
func (r *Repo) ReorderGroups(ctx context.Context, playlistID int, ids []int) (*Playlist, error) {
tx, err := r.pool.Begin(ctx)
if err != nil {
return nil, err
}
defer tx.Rollback(ctx)
if err := lockPlaylist(ctx, tx, playlistID); err != nil {
return nil, err
}
rows, err := tx.Query(
ctx,
`SELECT id
FROM playlist_groups
WHERE playlist_id = $1
ORDER BY position, id`,
playlistID,
)
if err != nil {
return nil, err
}
old, err := pgx.CollectRows(rows, pgx.RowTo[int])
if err != nil {
return nil, err
}
sortedOld, sortedNew := slices.Clone(old), slices.Clone(ids)
slices.Sort(sortedOld)
slices.Sort(sortedNew)
if !slices.Equal(sortedOld, sortedNew) {
return nil, ErrGroupOrderChanged
}
if !slices.Equal(old, ids) {
if err := groupInUse(ctx, tx, playlistID, 0); err != nil {
return nil, err
}
_, err = tx.Exec(
ctx,
`UPDATE playlist_groups g
SET position = ordered.position - 1
FROM unnest($2::int[]) WITH ORDINALITY AS ordered(id, position)
WHERE g.playlist_id = $1 AND g.id = ordered.id`,
playlistID,
ids,
)
if err != nil {
return nil, err
}
}
if err := tx.Commit(ctx); err != nil {
return nil, err
}
return r.Get(ctx, playlistID)
}
// SetTrackGroup moves a track without changing its global playback index.
func (r *Repo) SetTrackGroup(ctx context.Context, playlistID, trackID, groupID int) (*Playlist, error) {
tx, err := r.pool.Begin(ctx)
if err != nil {
return nil, err
}
defer tx.Rollback(ctx)
if err := lockPlaylist(ctx, tx, playlistID); err != nil {
return nil, err
}
var oldGroup int
err = tx.QueryRow(
ctx,
`SELECT COALESCE(group_id, 0)
FROM playlist_tracks
WHERE playlist_id = $1 AND id = $2`,
playlistID,
trackID,
).Scan(&oldGroup)
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrTrackNotFound
}
if err != nil {
return nil, err
}
if groupID != 0 {
if _, err := readGroup(ctx, tx, playlistID, groupID); err != nil {
return nil, err
}
}
if oldGroup != groupID {
// An ungrouped track must not block edits to unrelated active groups.
for _, id := range []int{oldGroup, groupID} {
if id != 0 {
if err := groupInUse(ctx, tx, playlistID, id); err != nil {
return nil, err
}
}
}
_, err = tx.Exec(
ctx,
`UPDATE playlist_tracks
SET group_id = NULLIF($3, 0)
WHERE playlist_id = $1 AND id = $2`,
playlistID,
trackID,
groupID,
)
if err != nil {
return nil, err
}
_, err = tx.Exec(
ctx,
`UPDATE game_music_states ms
SET group_id = NULL, state_version = state_version + 1
WHERE ms.playlist_id = $1 AND ms.playback = 1
AND ms.group_id = $2`,
playlistID,
oldGroup,
)
if err != nil {
return nil, err
}
}
if err := tx.Commit(ctx); err != nil {
return nil, err
}
return r.Get(ctx, playlistID)
}
@@ -0,0 +1,167 @@
package playlists_repo
import (
"context"
"errors"
"sync"
"sync/atomic"
"testing"
)
func TestGroupEditingProtectsLivePlayback(t *testing.T) {
for _, playback := range []int32{1, 2, 3} {
t.Run(map[int32]string{1: "stopped", 2: "playing", 3: "paused"}[playback], func(t *testing.T) {
f := newPlaylistFixture(t)
ctx := context.Background()
p, err := f.repo.Create(ctx, "Music")
requireNoError(t, err)
for _, name := range []string{"one", "two", "three"} {
requireNoError(t, f.repo.AddTrack(ctx, p.ID, name, name+".mp3"))
}
p, err = f.repo.SaveGroup(ctx, p.ID, 0, "First", "repeat")
requireNoError(t, err)
first := p.Groups[0].ID
p, err = f.repo.SaveGroup(ctx, p.ID, 0, "Second", "stop")
requireNoError(t, err)
second := p.Groups[1].ID
p, err = f.repo.SetTrackGroup(ctx, p.ID, p.Tracks[0].ID, first)
requireNoError(t, err)
p, err = f.repo.SetTrackGroup(ctx, p.ID, p.Tracks[1].ID, first)
requireNoError(t, err)
p, err = f.repo.SetTrackGroup(ctx, p.ID, p.Tracks[2].ID, second)
requireNoError(t, err)
state, err := f.repo.SetMusic(ctx, f.gameID, p.ID, 0, 123, 50, false, playback, first)
requireNoError(t, err)
_, err = f.repo.SaveGroup(ctx, p.ID, first, "Renamed", "repeat")
requireNoError(t, err)
for _, tc := range []struct {
name string
run func() error
}{
{"mode", func() error { _, err := f.repo.SaveGroup(ctx, p.ID, first, "Renamed", "next"); return err }},
{"group order", func() error { _, err := f.repo.ReorderGroups(ctx, p.ID, []int{second, first}); return err }},
{"track order", func() error {
_, err := f.repo.Reorder(ctx, p.ID, []int{p.Tracks[1].ID, p.Tracks[0].ID, p.Tracks[2].ID})
return err
}},
{"membership", func() error { _, err := f.repo.SetTrackGroup(ctx, p.ID, p.Tracks[0].ID, second); return err }},
{"delete group", func() error { _, err := f.repo.DeleteGroup(ctx, p.ID, first); return err }},
} {
t.Run(tc.name, func(t *testing.T) {
// Each stopped edit gets its own fixture state; live edits must be rejected atomically.
if playback == 1 {
return
}
if err := tc.run(); !errors.Is(err, ErrPlaylistInUse) {
t.Fatalf("got %v", err)
}
})
}
if playback != 1 {
_, err := f.repo.DeleteTrack(ctx, p.ID, p.Tracks[0].ID)
if !errors.Is(err, ErrPlaylistInUse) {
t.Fatal(err)
}
// An unrelated group's name and mode remain editable.
_, err = f.repo.SaveGroup(ctx, p.ID, second, "Other", "next")
requireNoError(t, err)
_, err = f.repo.SetMusic(ctx, f.gameID, p.ID, 0, 0, 50, false, 1, first)
requireNoError(t, err)
}
_, err = f.repo.SaveGroup(ctx, p.ID, first, "Renamed", "next")
requireNoError(t, err)
p, err = f.repo.ReorderGroups(ctx, p.ID, []int{second, first})
requireNoError(t, err)
if p.Groups[0].ID != second {
t.Fatal("group order lost")
}
p, err = f.repo.DeleteGroup(ctx, p.ID, first)
requireNoError(t, err)
if len(p.Tracks) != 3 || p.Tracks[0].GroupID != 0 || p.Tracks[1].GroupID != 0 {
t.Fatal("group deletion lost tracks")
}
after, err := f.repo.GetMusic(ctx, f.gameID)
requireNoError(t, err)
if after.GroupID != 0 {
t.Fatal("deleted group remains selected")
}
_, err = f.repo.DeleteTrack(ctx, p.ID, p.Tracks[0].ID)
requireNoError(t, err)
// A stale completion must never restore a deleted section.
_, err = f.repo.AdvanceMusic(ctx, f.gameID, state.Version, func(*MusicState) (int, int, int32) { t.Fatal("stale completion applied"); return 0, second, 2 })
requireNoError(t, err)
})
}
}
func TestGroupAssignmentsAndPlaybackRejectForeignIDs(t *testing.T) {
for _, name := range []string{"foreign and empty groups"} {
t.Run(name, func(t *testing.T) {
f := newPlaylistFixture(t)
ctx := context.Background()
p, err := f.repo.Create(ctx, "One")
requireNoError(t, err)
requireNoError(t, f.repo.AddTrack(ctx, p.ID, "Track", "track.mp3"))
p, err = f.repo.SaveGroup(ctx, p.ID, 0, "Empty", "repeat")
requireNoError(t, err)
other, err := f.repo.Create(ctx, "Other")
requireNoError(t, err)
other, err = f.repo.SaveGroup(ctx, other.ID, 0, "Foreign", "stop")
requireNoError(t, err)
_, err = f.repo.SetTrackGroup(ctx, p.ID, p.Tracks[0].ID, other.Groups[0].ID)
if !errors.Is(err, ErrGroupNotFound) {
t.Fatal(err)
}
_, err = f.repo.SetMusic(ctx, f.gameID, p.ID, 0, 0, 50, false, 2, p.Groups[0].ID)
if !errors.Is(err, ErrInvalidTrack) {
t.Fatal(err)
}
_, err = f.repo.ReorderGroups(ctx, p.ID, []int{other.Groups[0].ID})
if !errors.Is(err, ErrGroupOrderChanged) {
t.Fatal(err)
}
})
}
}
func TestConcurrentCompletionAdvancesOnce(t *testing.T) {
for _, playback := range []int32{1, 2, 3} {
t.Run(map[int32]string{1: "stopped", 2: "playing", 3: "paused"}[playback], func(t *testing.T) {
f := newPlaylistFixture(t)
ctx := context.Background()
p, err := f.repo.Create(ctx, "Concurrent completion")
requireNoError(t, err)
for _, name := range []string{"one", "two"} {
requireNoError(t, f.repo.AddTrack(ctx, p.ID, name, name+".mp3"))
}
before, err := f.repo.SetMusic(ctx, f.gameID, p.ID, 0, 0, 50, false, playback, 0)
requireNoError(t, err)
var calls atomic.Int32
failures := make(chan error, 8)
var wg sync.WaitGroup
for range 8 {
wg.Go(func() {
_, err := f.repo.AdvanceMusic(ctx, f.gameID, before.Version, func(state *MusicState) (int, int, int32) {
calls.Add(1)
return state.TrackIndex + 1, 0, state.Playback
})
failures <- err
})
}
wg.Wait()
close(failures)
for err := range failures {
requireNoError(t, err)
}
after, err := f.repo.GetMusic(ctx, f.gameID)
requireNoError(t, err)
want := int32(0)
if playback == 2 {
want = 1
}
if calls.Load() != want || after.TrackIndex != int(want) || after.Version != before.Version+int64(want) {
t.Fatalf("completion repeated or applied while stopped: calls=%d state=%+v", calls.Load(), after)
}
})
}
}
+29
View File
@@ -56,6 +56,35 @@ func (r *Repo) Reorder(ctx context.Context, playlistID int, trackIDs []int) (*Pl
return nil, ErrTrackOrderChanged return nil, ErrTrackOrderChanged
} }
if !slices.Equal(old, trackIDs) { if !slices.Equal(old, trackIDs) {
items, err := list(ctx, tx)
if err != nil {
return nil, err
}
for _, item := range items {
if item.ID != playlistID {
continue
}
for _, group := range item.Groups {
var before, after []int
members := make(map[int]bool)
for _, track := range item.Tracks {
if track.GroupID == group.ID {
before = append(before, track.ID)
members[track.ID] = true
}
}
for _, id := range trackIDs {
if members[id] {
after = append(after, id)
}
}
if !slices.Equal(before, after) {
if err := groupInUse(ctx, tx, playlistID, group.ID); err != nil {
return nil, err
}
}
}
}
// Lock state before changing indices; timing and playback remain untouched. // Lock state before changing indices; timing and playback remain untouched.
_, err = tx.Exec( _, err = tx.Exec(
ctx, ctx,
+4 -4
View File
@@ -24,12 +24,12 @@ func TestReorderPreservesPlaybackAndRejectsChangedMembership(t *testing.T) {
} }
p, err = f.repo.Get(ctx, p.ID) p, err = f.repo.Get(ctx, p.ID)
requireNoError(t, err) requireNoError(t, err)
before, err := f.repo.SetMusic(ctx, f.gameID, p.ID, 1, 1234, 37, true, playback) before, err := f.repo.SetMusic(ctx, f.gameID, p.ID, 1, 1234, 37, true, playback, 0)
requireNoError(t, err) requireNoError(t, err)
if err := f.repo.Delete(ctx, p.ID); !errors.Is(err, ErrPlaylistInUse) { if err := f.repo.Delete(ctx, p.ID); !errors.Is(err, ErrPlaylistInUse) {
t.Fatalf("active playlist deletion: %v", err) t.Fatalf("active playlist deletion: %v", err)
} }
if _, err := f.repo.DeleteTrack(ctx, p.ID, p.Tracks[0].ID); !errors.Is(err, ErrPlaylistInUse) { if _, err := f.repo.DeleteTrack(ctx, p.ID, 999999); !errors.Is(err, ErrTrackNotFound) {
t.Fatalf("active track deletion: %v", err) t.Fatalf("active track deletion: %v", err)
} }
order := []int{p.Tracks[1].ID, p.Tracks[2].ID, p.Tracks[0].ID} order := []int{p.Tracks[1].ID, p.Tracks[2].ID, p.Tracks[0].ID}
@@ -120,7 +120,7 @@ func TestBindInitializesAudioWithoutChangingTransport(t *testing.T) {
requireNoError(t, f.screens.Bind(ctx, first, f.gameID, "first")) requireNoError(t, f.screens.Bind(ctx, first, f.gameID, "first"))
requireNoError(t, f.screens.SetSound(ctx, first, true)) requireNoError(t, f.screens.SetSound(ctx, first, true))
requireNoError(t, f.screens.SetCommand(ctx, f.gameID, 2, 3456, 39)) requireNoError(t, f.screens.SetCommand(ctx, f.gameID, 2, 3456, 39))
before, err := f.repo.SetMusic(ctx, f.gameID, 0, 0, 1234, 42, true, 2) before, err := f.repo.SetMusic(ctx, f.gameID, 0, 0, 1234, 42, true, 2, 0)
requireNoError(t, err) requireNoError(t, err)
second, err := f.screens.Create(ctx, "second") second, err := f.screens.Create(ctx, "second")
requireNoError(t, err) requireNoError(t, err)
@@ -176,7 +176,7 @@ func TestReorderReadersSeeConsistentTrackAndIndex(t *testing.T) {
} }
p, err = f.repo.Get(ctx, p.ID) p, err = f.repo.Get(ctx, p.ID)
requireNoError(t, err) requireNoError(t, err)
_, err = f.repo.SetMusic(ctx, f.gameID, p.ID, 1, 1000, 50, true, 3) _, err = f.repo.SetMusic(ctx, f.gameID, p.ID, 1, 1000, 50, true, 3, 0)
requireNoError(t, err) requireNoError(t, err)
old := []int{p.Tracks[0].ID, p.Tracks[1].ID, p.Tracks[2].ID} old := []int{p.Tracks[0].ID, p.Tracks[1].ID, p.Tracks[2].ID}
reordered := []int{old[1], old[2], old[0]} reordered := []int{old[1], old[2], old[0]}
+189 -21
View File
@@ -10,7 +10,10 @@ import (
) )
var ( var (
ErrInvalidTrack = errors.New("некорректный трек или группа")
ErrGroupNotFound = errors.New("группа не найдена")
ErrPlaylistNotFound = errors.New("плейлист не найден") ErrPlaylistNotFound = errors.New("плейлист не найден")
ErrGroupOrderChanged = errors.New("состав групп изменился; обновите список")
ErrTrackOrderChanged = errors.New("состав треков изменился; обновите список") ErrTrackOrderChanged = errors.New("состав треков изменился; обновите список")
ErrPlaylistInUse = errors.New("плейлист используется") ErrPlaylistInUse = errors.New("плейлист используется")
ErrTrackNotFound = errors.New("трек не найден") ErrTrackNotFound = errors.New("трек не найден")
@@ -24,6 +27,15 @@ type Track struct {
Name string Name string
Filename string Filename string
Position int Position int
GroupID int
}
// Group describes an ordered section of a playlist.
type Group struct {
ID int
Name string
Position int
EndMode string
} }
// Playlist describes a stored playlist. // Playlist describes a stored playlist.
@@ -31,12 +43,14 @@ type Playlist struct {
ID int ID int
Name string Name string
Tracks []*Track Tracks []*Track
Groups []*Group
} }
// MusicState describes stored game music state. // MusicState describes stored game music state.
type MusicState struct { type MusicState struct {
Playlist *Playlist Playlist *Playlist
TrackIndex int TrackIndex int
GroupID int
PositionMs int64 PositionMs int64
Volume int32 Volume int32
Loop bool Loop bool
@@ -55,7 +69,12 @@ func New(pool *pgxpool.Pool) *Repo { return &Repo{pool: pool} }
// List returns all playlists with their tracks. // List returns all playlists with their tracks.
func (r *Repo) List(ctx context.Context) ([]*Playlist, error) { func (r *Repo) List(ctx context.Context) ([]*Playlist, error) {
return list(ctx, r.pool) tx, err := r.pool.BeginTx(ctx, pgx.TxOptions{IsoLevel: pgx.RepeatableRead, AccessMode: pgx.ReadOnly})
if err != nil {
return nil, err
}
defer tx.Rollback(ctx)
return list(ctx, tx)
} }
// IPlaylistReader reads catalogues from a pool or a consistent transaction snapshot. // IPlaylistReader reads catalogues from a pool or a consistent transaction snapshot.
@@ -72,7 +91,8 @@ func list(ctx context.Context, reader IPlaylistReader) ([]*Playlist, error) {
t.id, t.id,
t.name, t.name,
t.filename, t.filename,
t.position t.position,
COALESCE(t.group_id, 0)
FROM playlists p FROM playlists p
LEFT JOIN playlist_tracks t ON t.playlist_id = p.id LEFT JOIN playlist_tracks t ON t.playlist_id = p.id
ORDER BY p.id, t.position, t.id`, ORDER BY p.id, t.position, t.id`,
@@ -88,7 +108,8 @@ func list(ctx context.Context, reader IPlaylistReader) ([]*Playlist, error) {
var trackID *int var trackID *int
var trackName, filename *string var trackName, filename *string
var position *int var position *int
if err := rows.Scan(&p.ID, &p.Name, &trackID, &trackName, &filename, &position); err != nil { var groupID int
if err := rows.Scan(&p.ID, &p.Name, &trackID, &trackName, &filename, &position, &groupID); err != nil {
return nil, err return nil, err
} }
item := byID[p.ID] item := byID[p.ID]
@@ -98,10 +119,36 @@ func list(ctx context.Context, reader IPlaylistReader) ([]*Playlist, error) {
result = append(result, item) result = append(result, item)
} }
if trackID != nil { if trackID != nil {
item.Tracks = append(item.Tracks, &Track{ID: *trackID, Name: *trackName, Filename: *filename, Position: *position}) item.Tracks = append(item.Tracks, &Track{ID: *trackID, Name: *trackName, Filename: *filename, Position: *position, GroupID: groupID})
} }
} }
return result, rows.Err() if err := rows.Err(); err != nil {
return nil, err
}
rows.Close()
groups, err := reader.Query(ctx, `SELECT
playlist_id,
id,
name,
position,
end_mode
FROM playlist_groups
ORDER BY playlist_id, position, id`)
if err != nil {
return nil, err
}
defer groups.Close()
for groups.Next() {
var playlistID int
var group Group
if err := groups.Scan(&playlistID, &group.ID, &group.Name, &group.Position, &group.EndMode); err != nil {
return nil, err
}
if p := byID[playlistID]; p != nil {
p.Groups = append(p.Groups, &group)
}
}
return result, groups.Err()
} }
// Get returns a playlist by ID. // Get returns a playlist by ID.
@@ -244,20 +291,59 @@ func (r *Repo) DeleteTrack(ctx context.Context, playlistID, trackID int) (string
return "", err return "", err
} }
var filename string var filename string
var used bool var groupID int
if err = tx.QueryRow( err = tx.QueryRow(
ctx, ctx,
` `SELECT COALESCE(group_id, 0)
SELECT EXISTS (SELECT 1 FROM scenarios WHERE playlist_id = $1) FROM playlist_tracks
OR EXISTS (SELECT 1 FROM games WHERE playlist_id = $1) WHERE playlist_id = $1 AND id = $2`,
OR EXISTS (SELECT 1 FROM game_music_states WHERE playlist_id = $1)
`,
playlistID, playlistID,
).Scan(&used); err != nil { trackID,
).Scan(&groupID)
if errors.Is(err, pgx.ErrNoRows) {
return "", ErrTrackNotFound
}
if err != nil {
return "", err return "", err
} }
if used { if groupID == 0 {
return "", ErrPlaylistInUse groupID = -1
}
if err := groupInUse(ctx, tx, playlistID, groupID); err != nil {
return "", err
}
_, err = tx.Exec(
ctx,
`WITH ordered AS (
SELECT
id,
row_number() OVER (ORDER BY position, id) - 1 AS index,
count(*) OVER () AS total
FROM playlist_tracks
WHERE playlist_id = $1
), removed AS (
SELECT index, total
FROM ordered
WHERE id = $2
)
UPDATE game_music_states ms
SET track_index = CASE
WHEN ms.track_index > removed.index THEN ms.track_index - 1
WHEN ms.track_index = removed.index THEN GREATEST(0, LEAST(ms.track_index, removed.total - 2))
ELSE ms.track_index END,
group_id = CASE WHEN ms.track_index = removed.index THEN NULL ELSE ms.group_id END,
position_ms = CASE WHEN ms.track_index = removed.index THEN 0 ELSE ms.position_ms END,
state_version = ms.state_version + 1
FROM games g
JOIN scenarios s ON s.id = g.scenario_id
CROSS JOIN removed
WHERE ms.game_id = g.id
AND COALESCE(ms.playlist_id, g.playlist_id, s.playlist_id) = $1`,
playlistID,
trackID,
)
if err != nil {
return "", err
} }
err = tx.QueryRow( err = tx.QueryRow(
ctx, ctx,
@@ -327,6 +413,10 @@ func (r *Repo) GetMusic(ctx context.Context, gameID int) (*MusicState, error) {
return nil, err return nil, err
} }
defer tx.Rollback(ctx) defer tx.Rollback(ctx)
return getMusic(ctx, tx, gameID)
}
func getMusic(ctx context.Context, tx pgx.Tx, gameID int) (*MusicState, error) {
playlists, err := list(ctx, tx) playlists, err := list(ctx, tx)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -382,13 +472,14 @@ func (r *Repo) GetMusic(ctx context.Context, gameID int) (*MusicState, error) {
loop, loop,
playback, playback,
state_version, state_version,
started_at started_at,
COALESCE(group_id, 0)
FROM game_music_states FROM game_music_states
WHERE game_id = $1 WHERE game_id = $1
`, `,
gameID, gameID,
). ).
Scan(&statePlaylistID, &trackIndex, &state.PositionMs, &state.Volume, &state.Loop, &state.Playback, &state.Version, &state.StartedAt) Scan(&statePlaylistID, &trackIndex, &state.PositionMs, &state.Volume, &state.Loop, &state.Playback, &state.Version, &state.StartedAt, &state.GroupID)
if errors.Is(err, pgx.ErrNoRows) { if errors.Is(err, pgx.ErrNoRows) {
state.Version = 1 state.Version = 1
return state, nil return state, nil
@@ -410,7 +501,7 @@ func (r *Repo) GetMusic(ctx context.Context, gameID int) (*MusicState, error) {
} }
// SetMusic stores the current music state for a game. // SetMusic stores the current music state for a game.
func (r *Repo) SetMusic(ctx context.Context, gameID, playlistID, trackIndex int, positionMs int64, volume int32, loop bool, playback int32) (*MusicState, error) { func (r *Repo) SetMusic(ctx context.Context, gameID, playlistID, trackIndex int, positionMs int64, volume int32, loop bool, playback int32, groupID int) (*MusicState, error) {
tx, err := r.pool.Begin(ctx) tx, err := r.pool.Begin(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -421,19 +512,55 @@ func (r *Repo) SetMusic(ctx context.Context, gameID, playlistID, trackIndex int,
return nil, err return nil, err
} }
} }
_, err = tx.Exec( if playlistID < 0 || groupID < 0 {
return nil, ErrInvalidTrack
}
if playlistID > 0 {
items, err := list(ctx, tx)
if err != nil {
return nil, err
}
for _, item := range items {
if item.ID != playlistID {
continue
}
if trackIndex < 0 || (len(item.Tracks) == 0 && trackIndex != 0) || (len(item.Tracks) > 0 && trackIndex >= len(item.Tracks)) || (len(item.Tracks) == 0 && playback != 1) {
return nil, ErrInvalidTrack
}
if groupID != 0 {
if trackIndex >= len(item.Tracks) || item.Tracks[trackIndex].GroupID != groupID {
return nil, ErrInvalidTrack
}
}
}
} else if groupID != 0 {
return nil, ErrInvalidTrack
}
err = writeMusic(ctx, tx, gameID, playlistID, trackIndex, positionMs, volume, loop, playback, groupID)
if err != nil {
return nil, err
}
if err := tx.Commit(ctx); err != nil {
return nil, err
}
return r.GetMusic(ctx, gameID)
}
func writeMusic(ctx context.Context, tx pgx.Tx, gameID, playlistID, trackIndex int, positionMs int64, volume int32, loop bool, playback int32, groupID int) error {
_, err := tx.Exec(
ctx, ctx,
` `
INSERT INTO game_music_states ( INSERT INTO game_music_states (
game_id, playlist_id, track_index, position_ms, volume, loop, game_id, playlist_id, track_index, position_ms, volume, loop,
playback, state_version, started_at playback, state_version, started_at, group_id
) )
VALUES ( VALUES (
$1, NULLIF($2, 0), $3, $4, $5, $6, $7, 1, $1, NULLIF($2, 0), $3, $4, $5, $6, $7, 1,
CASE WHEN $7 = 2 THEN NOW() ELSE NULL END CASE WHEN $7 = 2 THEN NOW() ELSE NULL END, NULLIF($8, 0)
) )
ON CONFLICT (game_id) DO UPDATE SET ON CONFLICT (game_id) DO UPDATE SET
playlist_id = EXCLUDED.playlist_id, playlist_id = EXCLUDED.playlist_id,
group_id = EXCLUDED.group_id,
track_index = EXCLUDED.track_index, track_index = EXCLUDED.track_index,
position_ms = EXCLUDED.position_ms, position_ms = EXCLUDED.position_ms,
volume = EXCLUDED.volume, volume = EXCLUDED.volume,
@@ -450,10 +577,51 @@ func (r *Repo) SetMusic(ctx context.Context, gameID, playlistID, trackIndex int,
volume, volume,
loop, loop,
playback, playback,
groupID,
) )
return err
}
// AdvanceMusic keeps the catalogue and current command locked while the service chooses a transition.
func (r *Repo) AdvanceMusic(ctx context.Context, gameID int, expectedVersion int64, next func(*MusicState) (int, int, int32)) (*MusicState, error) {
before, err := r.GetMusic(ctx, gameID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if before.Playlist == nil || before.Playback != 2 {
return before, nil
}
tx, err := r.pool.Begin(ctx)
if err != nil {
return nil, err
}
defer tx.Rollback(ctx)
if err := lockPlaylist(ctx, tx, before.Playlist.ID); err != nil {
return nil, err
}
var id int
err = tx.QueryRow(
ctx,
`SELECT game_id
FROM game_music_states
WHERE game_id = $1
FOR UPDATE`,
gameID,
).Scan(&id)
if err != nil {
return nil, err
}
state, err := getMusic(ctx, tx, gameID)
if err != nil {
return nil, err
}
if state.Playlist == nil || state.Playlist.ID != before.Playlist.ID || state.Playback != 2 || len(state.Playlist.Tracks) == 0 || (expectedVersion != 0 && expectedVersion != state.Version) {
return state, nil
}
index, groupID, playback := next(state)
if err := writeMusic(ctx, tx, gameID, state.Playlist.ID, index, 0, state.Volume, state.Loop, playback, groupID); err != nil {
return nil, err
}
if err := tx.Commit(ctx); err != nil { if err := tx.Commit(ctx); err != nil {
return nil, err return nil, err
} }
@@ -317,6 +317,11 @@ func validateLocalIDs(data *game_package_service.Data) error {
if playlist.ID > maxID { if playlist.ID > maxID {
return fmt.Errorf("%w: playlist ID", game_package_service.ErrInvalidPackage) return fmt.Errorf("%w: playlist ID", game_package_service.ErrInvalidPackage)
} }
for _, group := range playlist.Groups {
if group.ID > maxID || int64(group.Position) > int64(maxID) {
return fmt.Errorf("%w: group ID or position", game_package_service.ErrInvalidPackage)
}
}
for _, track := range playlist.Tracks { for _, track := range playlist.Tracks {
if track.ID > maxID { if track.ID > maxID {
return fmt.Errorf("%w: track ID", game_package_service.ErrInvalidPackage) return fmt.Errorf("%w: track ID", game_package_service.ErrInvalidPackage)
@@ -0,0 +1,53 @@
package game_package_service
import (
"bytes"
"context"
"errors"
"os"
"reflect"
"testing"
)
func TestPackagePreservesGroupOrderAndMembership(t *testing.T) {
for _, name := range []string{"group roundtrip"} {
t.Run(name, func(t *testing.T) {
snapshot, files := fixture()
snapshot.Data.Playlists[0].Groups = []PlaylistGroup{{ID: 80, Name: "Final", Position: 0, EndMode: "stop"}, {ID: 81, Name: "Intro", Position: 1, EndMode: "next"}, {ID: 82, Name: "Empty", Position: 2, EndMode: "repeat"}}
snapshot.Data.Playlists[0].Tracks[0].GroupID = 81
service := NewGamePackageService("")
raw := buildPackage(t, service, snapshot, files)
bundle, err := service.Unpack(context.Background(), bytes.NewReader(raw), int64(len(raw)), t.TempDir())
if err != nil {
t.Fatal(err)
}
defer func() { _ = os.RemoveAll(bundle.Dir) }()
if !reflect.DeepEqual(bundle.Data.Playlists[0].Groups, snapshot.Data.Playlists[0].Groups) || bundle.Data.Playlists[0].Tracks[0].GroupID != 81 {
t.Fatal("group data changed")
}
})
}
}
func TestPackRejectsInvalidGroups(t *testing.T) {
cases := []struct {
name string
modify func(*Playlist)
}{
{"unknown mode", func(p *Playlist) { p.Groups[0].EndMode = "unknown" }},
{"duplicate ID", func(p *Playlist) { p.Groups = append(p.Groups, p.Groups[0]) }},
{"missing group", func(p *Playlist) { p.Tracks[0].GroupID = 999 }},
{"negative position", func(p *Playlist) { p.Groups[0].Position = -1 }},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
snapshot, files := fixture()
snapshot.Data.Playlists[0].Groups = []PlaylistGroup{{ID: 80, Name: "Group", EndMode: "repeat"}}
tc.modify(&snapshot.Data.Playlists[0])
err := NewGamePackageService("").Pack(context.Background(), snapshot, localFiles(files, nil), &bytes.Buffer{})
if !errors.Is(err, ErrInvalidPackage) {
t.Fatal(err)
}
})
}
}
@@ -107,9 +107,17 @@ type Ticket struct {
} }
type Playlist struct { type Playlist struct {
ID int `json:"id"` ID int `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Tracks []Track `json:"tracks"` Tracks []Track `json:"tracks"`
Groups []PlaylistGroup `json:"groups,omitempty"`
}
type PlaylistGroup struct {
ID int `json:"id"`
Name string `json:"name"`
Position int `json:"position"`
EndMode string `json:"end_mode"`
} }
type Track struct { type Track struct {
@@ -117,6 +125,7 @@ type Track struct {
Name string `json:"name"` Name string `json:"name"`
Filename string `json:"filename"` Filename string `json:"filename"`
Position int `json:"position"` Position int `json:"position"`
GroupID int `json:"group_id,omitempty"`
} }
type ScreenPlan struct { type ScreenPlan struct {
@@ -103,15 +103,26 @@ func validateData(data *Data) error {
} }
playlists := map[int]bool{} playlists := map[int]bool{}
tracks := map[int]bool{} tracks := map[int]bool{}
groups := map[int]bool{}
for _, playlist := range data.Playlists { for _, playlist := range data.Playlists {
if playlist.ID <= 0 || playlists[playlist.ID] || strings.TrimSpace(playlist.Name) == "" { if playlist.ID <= 0 || playlists[playlist.ID] || strings.TrimSpace(playlist.Name) == "" {
return invalid("некорректный плейлист") return invalid("некорректный плейлист")
} }
playlists[playlist.ID] = true playlists[playlist.ID] = true
localGroups := map[int]bool{}
for _, group := range playlist.Groups {
if group.ID <= 0 || groups[group.ID] || strings.TrimSpace(group.Name) == "" || len(group.Name) > 255 || group.Position < 0 || (group.EndMode != "repeat" && group.EndMode != "stop" && group.EndMode != "next") {
return invalid("некорректная группа плейлиста")
}
groups[group.ID], localGroups[group.ID] = true, true
}
for _, track := range playlist.Tracks { for _, track := range playlist.Tracks {
if track.ID <= 0 || tracks[track.ID] || strings.TrimSpace(track.Name) == "" || track.Filename == "" || track.Position < 0 { if track.ID <= 0 || tracks[track.ID] || strings.TrimSpace(track.Name) == "" || track.Filename == "" || track.Position < 0 {
return invalid("некорректный трек") return invalid("некорректный трек")
} }
if track.GroupID != 0 && !localGroups[track.GroupID] {
return invalid("группа трека не принадлежит плейлисту")
}
tracks[track.ID] = true tracks[track.ID] = true
} }
} }
@@ -0,0 +1,118 @@
package playlist_service
import (
"context"
"strconv"
"strings"
"evening_detective_server/internal/repos/playlists_repo"
)
// SaveGroup validates group metadata before persisting it.
func (s *Service) SaveGroup(ctx context.Context, playlistID, groupID int, name, mode string) (*Playlist, error) {
name = strings.TrimSpace(name)
if playlistID <= 0 || groupID < 0 || name == "" || len(name) > 255 {
return nil, ErrInvalidName
}
if mode != "repeat" && mode != "stop" && mode != "next" {
return nil, ErrInvalidTrack
}
item, err := s.repo.SaveGroup(ctx, playlistID, groupID, name, mode)
return mapPlaylist(item), mapError(err)
}
// DeleteGroup preserves tracks when removing their section.
func (s *Service) DeleteGroup(ctx context.Context, playlistID, groupID int) (*Playlist, error) {
if playlistID <= 0 || groupID <= 0 {
return nil, ErrInvalidTrack
}
item, err := s.repo.DeleteGroup(ctx, playlistID, groupID)
return mapPlaylist(item), mapError(err)
}
// ReorderGroups validates the complete requested group order.
func (s *Service) ReorderGroups(ctx context.Context, playlistID int, ids []int) (*Playlist, error) {
if playlistID <= 0 {
return nil, ErrInvalidTrack
}
seen := make(map[int]bool, len(ids))
for _, id := range ids {
if id <= 0 || seen[id] {
return nil, ErrInvalidTrack
}
seen[id] = true
}
item, err := s.repo.ReorderGroups(ctx, playlistID, ids)
return mapPlaylist(item), mapError(err)
}
// SetTrackGroup validates track and section identifiers.
func (s *Service) SetTrackGroup(ctx context.Context, playlistID, trackID, groupID int) (*Playlist, error) {
if playlistID <= 0 || trackID <= 0 || groupID < 0 {
return nil, ErrInvalidTrack
}
item, err := s.repo.SetTrackGroup(ctx, playlistID, trackID, groupID)
return mapPlaylist(item), mapError(err)
}
// nextTrack calculates the next global index while respecting section boundaries.
func nextTrack(state *MusicState) (index, groupID int, playback int32) {
index, groupID, playback = state.TrackIndex, state.GroupID, state.Playback
if state.Playlist == nil || len(state.Playlist.Tracks) == 0 {
return index, groupID, 1
}
tracks := state.Playlist.Tracks
if groupID == 0 {
if index+1 < len(tracks) {
return index + 1, 0, playback
}
if state.Loop {
return 0, 0, playback
}
return index, 0, 1
}
for i := index + 1; i < len(tracks); i++ {
if tracks[i].GroupID == groupID {
return i, groupID, playback
}
}
for position, group := range state.Playlist.Groups {
if group.ID != groupID {
continue
}
if group.EndMode == "repeat" {
for i, track := range tracks {
if track.GroupID == groupID {
return i, groupID, playback
}
}
}
if group.EndMode == "next" {
for _, next := range state.Playlist.Groups[position+1:] {
for i, track := range tracks {
if track.GroupID == next.ID {
return i, next.ID, playback
}
}
}
}
break
}
return index, groupID, 1
}
// Advance applies a completion event only to the state the screen played.
func (s *Service) Advance(ctx context.Context, gameID int, expectedStateVersion string) (*MusicState, error) {
var expectedVersion int64
if expectedStateVersion != "" {
var err error
expectedVersion, err = strconv.ParseInt(expectedStateVersion, 10, 64)
if err != nil || expectedVersion < 0 {
return nil, ErrInvalidTrack
}
}
updated, err := s.repo.AdvanceMusic(ctx, gameID, expectedVersion, func(state *playlists_repo.MusicState) (int, int, int32) {
return nextTrack(mapMusicState(state))
})
return mapMusicState(updated), mapError(err)
}
@@ -0,0 +1,77 @@
package playlist_service
import (
"context"
"errors"
"testing"
)
func TestNextTrackHonorsGroupCompletion(t *testing.T) {
cases := []struct {
name, mode string
index, groupID int
loop bool
groups []*Group
wantIndex, wantGroup int
wantPlayback int32
}{
{name: "within group", mode: "stop", index: 0, groupID: 1, wantIndex: 2, wantGroup: 1, wantPlayback: 2},
{name: "repeat", mode: "repeat", index: 2, groupID: 1, wantIndex: 0, wantGroup: 1, wantPlayback: 2},
{name: "stop", mode: "stop", index: 2, groupID: 1, wantIndex: 2, wantGroup: 1, wantPlayback: 1},
{name: "next skips empty", mode: "next", index: 2, groupID: 1, wantIndex: 1, wantGroup: 3, wantPlayback: 2},
{name: "last group stops", index: 1, groupID: 3, wantIndex: 1, wantGroup: 3, wantPlayback: 1},
{name: "reordered next", mode: "next", index: 2, groupID: 1, groups: []*Group{{ID: 3, EndMode: "repeat"}, {ID: 1, EndMode: "next"}}, wantIndex: 2, wantGroup: 1, wantPlayback: 1},
{name: "single track repeats", index: 1, groupID: 3, groups: []*Group{{ID: 3, EndMode: "repeat"}}, wantIndex: 1, wantGroup: 3, wantPlayback: 2},
{name: "whole playlist", index: 0, wantIndex: 1, wantPlayback: 2},
{name: "whole playlist repeats", index: 2, loop: true, wantIndex: 0, wantPlayback: 2},
{name: "whole playlist stops", index: 2, wantIndex: 2, wantPlayback: 1},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
groups := tc.groups
if groups == nil {
groups = []*Group{{ID: 1, EndMode: tc.mode}, {ID: 2, EndMode: "repeat"}, {ID: 3, EndMode: "next"}}
}
state := &MusicState{TrackIndex: tc.index, GroupID: tc.groupID, Playback: 2, Loop: tc.loop, Playlist: &Playlist{Tracks: []*Track{{ID: 10, GroupID: 1}, {ID: 11, GroupID: 3}, {ID: 12, GroupID: 1}}, Groups: groups}}
index, groupID, playback := nextTrack(state)
if index != tc.wantIndex || groupID != tc.wantGroup || playback != tc.wantPlayback {
t.Fatalf("got %d/%d/%d", index, groupID, playback)
}
})
}
}
func TestGroupValidationRejectsInvalidRequestsBeforeRepository(t *testing.T) {
cases := []struct {
name string
run func(*Service) error
want error
}{
{"empty name", func(s *Service) error { _, err := s.SaveGroup(context.Background(), 1, 0, " ", "repeat"); return err }, ErrInvalidName},
{"invalid mode", func(s *Service) error {
_, err := s.SaveGroup(context.Background(), 1, 0, "Name", "unknown")
return err
}, ErrInvalidTrack},
{"invalid group", func(s *Service) error { _, err := s.DeleteGroup(context.Background(), 1, 0); return err }, ErrInvalidTrack},
{"duplicate order", func(s *Service) error { _, err := s.ReorderGroups(context.Background(), 1, []int{1, 1}); return err }, ErrInvalidTrack},
{"invalid assignment", func(s *Service) error { _, err := s.SetTrackGroup(context.Background(), 1, 2, -1); return err }, ErrInvalidTrack},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if err := tc.run(New(nil, nil)); !errors.Is(err, tc.want) {
t.Fatal(err)
}
})
}
}
func TestAdvanceRejectsInvalidCompletionVersion(t *testing.T) {
for _, value := range []string{"-1", "invalid", "9223372036854775808"} {
t.Run(value, func(t *testing.T) {
_, err := New(nil, nil).Advance(context.Background(), 1, value)
if !errors.Is(err, ErrInvalidTrack) {
t.Fatal(err)
}
})
}
}
@@ -13,8 +13,12 @@ func mapPlaylist(value *playlists_repo.Playlist) *Playlist {
Name: track.Name, Name: track.Name,
Filename: track.Filename, Filename: track.Filename,
Position: track.Position, Position: track.Position,
GroupID: track.GroupID,
}) })
} }
for _, group := range value.Groups {
result.Groups = append(result.Groups, &Group{ID: group.ID, Name: group.Name, Position: group.Position, EndMode: group.EndMode})
}
return result return result
} }
@@ -25,6 +29,7 @@ func mapMusicState(value *playlists_repo.MusicState) *MusicState {
return &MusicState{ return &MusicState{
Playlist: mapPlaylist(value.Playlist), Playlist: mapPlaylist(value.Playlist),
TrackIndex: value.TrackIndex, TrackIndex: value.TrackIndex,
GroupID: value.GroupID,
PositionMs: value.PositionMs, PositionMs: value.PositionMs,
Volume: value.Volume, Volume: value.Volume,
Loop: value.Loop, Loop: value.Loop,
@@ -8,6 +8,15 @@ type Track struct {
Name string Name string
Filename string Filename string
Position int Position int
GroupID int
}
// Group describes an ordered section of a playlist.
type Group struct {
ID int
Name string
Position int
EndMode string
} }
// Playlist describes a reusable playlist. // Playlist describes a reusable playlist.
@@ -15,12 +24,14 @@ type Playlist struct {
ID int ID int
Name string Name string
Tracks []*Track Tracks []*Track
Groups []*Group
} }
// MusicState describes the synchronized music state of a game. // MusicState describes the synchronized music state of a game.
type MusicState struct { type MusicState struct {
Playlist *Playlist Playlist *Playlist
TrackIndex int TrackIndex int
GroupID int
PositionMs int64 PositionMs int64
Volume int32 Volume int32
Loop bool Loop bool
@@ -27,7 +27,7 @@ func TestReorderRejectsInvalidIDsBeforeRepository(t *testing.T) {
} }
func TestOrderConflictMapping(t *testing.T) { func TestOrderConflictMapping(t *testing.T) {
for _, tc := range []struct{ input, expected error }{{playlists_repo.ErrTrackOrderChanged, ErrTrackOrderChanged}} { for _, tc := range []struct{ input, expected error }{{playlists_repo.ErrTrackOrderChanged, ErrTrackOrderChanged}, {playlists_repo.ErrGroupOrderChanged, ErrGroupOrderChanged}, {playlists_repo.ErrGroupNotFound, ErrGroupNotFound}, {playlists_repo.ErrInvalidTrack, ErrInvalidTrack}} {
if !errors.Is(mapError(tc.input), tc.expected) { if !errors.Is(mapError(tc.input), tc.expected) {
t.Fatal("lost conflict") t.Fatal("lost conflict")
} }
+10 -1
View File
@@ -11,6 +11,8 @@ import (
) )
var ( var (
ErrGroupNotFound = errors.New("группа не найдена")
ErrGroupOrderChanged = errors.New("состав групп изменился; обновите список")
ErrTrackOrderChanged = errors.New("состав треков изменился; обновите список") ErrTrackOrderChanged = errors.New("состав треков изменился; обновите список")
ErrInvalidName = errors.New("некорректное имя плейлиста") ErrInvalidName = errors.New("некорректное имя плейлиста")
ErrInvalidTrack = errors.New("некорректный трек") ErrInvalidTrack = errors.New("некорректный трек")
@@ -129,11 +131,12 @@ func (s *Service) SetGame(
volume int32, volume int32,
loop bool, loop bool,
playback int32, playback int32,
groupID int,
) (*MusicState, error) { ) (*MusicState, error) {
if trackIndex < 0 || positionMs < 0 || volume < 0 || volume > 100 || playback < 1 || playback > 3 { if trackIndex < 0 || positionMs < 0 || volume < 0 || volume > 100 || playback < 1 || playback > 3 {
return nil, ErrInvalidTrack return nil, ErrInvalidTrack
} }
state, err := s.repo.SetMusic(ctx, gameID, playlistID, trackIndex, positionMs, volume, loop, playback) state, err := s.repo.SetMusic(ctx, gameID, playlistID, trackIndex, positionMs, volume, loop, playback, groupID)
if err != nil { if err != nil {
return nil, mapError(err) return nil, mapError(err)
} }
@@ -151,6 +154,12 @@ func (s *Service) GetGame(ctx context.Context, gameID int) (*MusicState, error)
func mapError(err error) error { func mapError(err error) error {
switch { switch {
case errors.Is(err, playlists_repo.ErrGroupOrderChanged):
return ErrGroupOrderChanged
case errors.Is(err, playlists_repo.ErrInvalidTrack):
return ErrInvalidTrack
case errors.Is(err, playlists_repo.ErrGroupNotFound):
return ErrGroupNotFound
case errors.Is(err, playlists_repo.ErrTrackOrderChanged): case errors.Is(err, playlists_repo.ErrTrackOrderChanged):
return ErrTrackOrderChanged return ErrTrackOrderChanged
case errors.Is(err, playlists_repo.ErrPlaylistNotFound): case errors.Is(err, playlists_repo.ErrPlaylistNotFound):
@@ -0,0 +1,20 @@
-- +goose Up
CREATE TABLE playlist_groups (
id SERIAL PRIMARY KEY,
playlist_id INTEGER NOT NULL REFERENCES playlists(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL CHECK (length(trim(name)) > 0),
position INTEGER NOT NULL CHECK (position >= 0),
end_mode TEXT NOT NULL DEFAULT 'repeat' CHECK (end_mode IN ('repeat', 'stop', 'next')),
UNIQUE (playlist_id, id)
);
ALTER TABLE playlist_tracks ADD COLUMN group_id INTEGER;
ALTER TABLE playlist_tracks ADD CONSTRAINT playlist_tracks_group_fk
FOREIGN KEY (playlist_id, group_id) REFERENCES playlist_groups(playlist_id, id);
ALTER TABLE game_music_states ADD COLUMN group_id INTEGER;
ALTER TABLE game_music_states ADD CONSTRAINT game_music_states_group_fk
FOREIGN KEY (playlist_id, group_id) REFERENCES playlist_groups(playlist_id, id);
-- +goose Down
ALTER TABLE game_music_states DROP COLUMN group_id;
ALTER TABLE playlist_tracks DROP COLUMN group_id;
DROP TABLE playlist_groups;
+1933 -1290
View File
File diff suppressed because it is too large Load Diff
+362
View File
@@ -907,6 +907,212 @@ func local_request_EveningDetectiveServer_DownloadFile_0(ctx context.Context, ma
return msg, metadata, err return msg, metadata, err
} }
func request_EveningDetectiveServer_SavePlaylistGroup_0(ctx context.Context, marshaler runtime.Marshaler, client EveningDetectiveServerClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq SavePlaylistGroupReq
metadata runtime.ServerMetadata
err error
)
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if req.Body != nil {
_, _ = io.Copy(io.Discard, req.Body)
}
val, ok := pathParams["playlistID"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "playlistID")
}
protoReq.PlaylistID, err = runtime.Int32(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "playlistID", err)
}
msg, err := client.SavePlaylistGroup(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_EveningDetectiveServer_SavePlaylistGroup_0(ctx context.Context, marshaler runtime.Marshaler, server EveningDetectiveServerServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq SavePlaylistGroupReq
metadata runtime.ServerMetadata
err error
)
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
val, ok := pathParams["playlistID"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "playlistID")
}
protoReq.PlaylistID, err = runtime.Int32(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "playlistID", err)
}
msg, err := server.SavePlaylistGroup(ctx, &protoReq)
return msg, metadata, err
}
func request_EveningDetectiveServer_DeletePlaylistGroup_0(ctx context.Context, marshaler runtime.Marshaler, client EveningDetectiveServerClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq DeletePlaylistGroupReq
metadata runtime.ServerMetadata
err error
)
if req.Body != nil {
_, _ = io.Copy(io.Discard, req.Body)
}
val, ok := pathParams["playlistID"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "playlistID")
}
protoReq.PlaylistID, err = runtime.Int32(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "playlistID", err)
}
val, ok = pathParams["groupID"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "groupID")
}
protoReq.GroupID, err = runtime.Int32(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "groupID", err)
}
msg, err := client.DeletePlaylistGroup(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_EveningDetectiveServer_DeletePlaylistGroup_0(ctx context.Context, marshaler runtime.Marshaler, server EveningDetectiveServerServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq DeletePlaylistGroupReq
metadata runtime.ServerMetadata
err error
)
val, ok := pathParams["playlistID"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "playlistID")
}
protoReq.PlaylistID, err = runtime.Int32(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "playlistID", err)
}
val, ok = pathParams["groupID"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "groupID")
}
protoReq.GroupID, err = runtime.Int32(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "groupID", err)
}
msg, err := server.DeletePlaylistGroup(ctx, &protoReq)
return msg, metadata, err
}
func request_EveningDetectiveServer_ReorderPlaylistGroups_0(ctx context.Context, marshaler runtime.Marshaler, client EveningDetectiveServerClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq ReorderPlaylistGroupsReq
metadata runtime.ServerMetadata
err error
)
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if req.Body != nil {
_, _ = io.Copy(io.Discard, req.Body)
}
val, ok := pathParams["playlistID"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "playlistID")
}
protoReq.PlaylistID, err = runtime.Int32(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "playlistID", err)
}
msg, err := client.ReorderPlaylistGroups(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_EveningDetectiveServer_ReorderPlaylistGroups_0(ctx context.Context, marshaler runtime.Marshaler, server EveningDetectiveServerServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq ReorderPlaylistGroupsReq
metadata runtime.ServerMetadata
err error
)
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
val, ok := pathParams["playlistID"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "playlistID")
}
protoReq.PlaylistID, err = runtime.Int32(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "playlistID", err)
}
msg, err := server.ReorderPlaylistGroups(ctx, &protoReq)
return msg, metadata, err
}
func request_EveningDetectiveServer_SetPlaylistTrackGroup_0(ctx context.Context, marshaler runtime.Marshaler, client EveningDetectiveServerClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq SetPlaylistTrackGroupReq
metadata runtime.ServerMetadata
err error
)
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if req.Body != nil {
_, _ = io.Copy(io.Discard, req.Body)
}
val, ok := pathParams["playlistID"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "playlistID")
}
protoReq.PlaylistID, err = runtime.Int32(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "playlistID", err)
}
val, ok = pathParams["trackID"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "trackID")
}
protoReq.TrackID, err = runtime.Int32(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "trackID", err)
}
msg, err := client.SetPlaylistTrackGroup(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_EveningDetectiveServer_SetPlaylistTrackGroup_0(ctx context.Context, marshaler runtime.Marshaler, server EveningDetectiveServerServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq SetPlaylistTrackGroupReq
metadata runtime.ServerMetadata
err error
)
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
val, ok := pathParams["playlistID"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "playlistID")
}
protoReq.PlaylistID, err = runtime.Int32(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "playlistID", err)
}
val, ok = pathParams["trackID"]
if !ok {
return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "trackID")
}
protoReq.TrackID, err = runtime.Int32(val)
if err != nil {
return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "trackID", err)
}
msg, err := server.SetPlaylistTrackGroup(ctx, &protoReq)
return msg, metadata, err
}
func request_EveningDetectiveServer_GetPlaylists_0(ctx context.Context, marshaler runtime.Marshaler, client EveningDetectiveServerClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { func request_EveningDetectiveServer_GetPlaylists_0(ctx context.Context, marshaler runtime.Marshaler, client EveningDetectiveServerClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var ( var (
protoReq GetPlaylistsReq protoReq GetPlaylistsReq
@@ -5149,6 +5355,86 @@ func RegisterEveningDetectiveServerHandlerServer(ctx context.Context, mux *runti
} }
forward_EveningDetectiveServer_DownloadFile_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) forward_EveningDetectiveServer_DownloadFile_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
}) })
mux.Handle(http.MethodPost, pattern_EveningDetectiveServer_SavePlaylistGroup_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/crabs.evening_detective_server.EveningDetectiveServer/SavePlaylistGroup", runtime.WithHTTPPathPattern("/api/playlists/{playlistID}/groups"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_EveningDetectiveServer_SavePlaylistGroup_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_EveningDetectiveServer_SavePlaylistGroup_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodDelete, pattern_EveningDetectiveServer_DeletePlaylistGroup_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/crabs.evening_detective_server.EveningDetectiveServer/DeletePlaylistGroup", runtime.WithHTTPPathPattern("/api/playlists/{playlistID}/groups/{groupID}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_EveningDetectiveServer_DeletePlaylistGroup_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_EveningDetectiveServer_DeletePlaylistGroup_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodPut, pattern_EveningDetectiveServer_ReorderPlaylistGroups_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/crabs.evening_detective_server.EveningDetectiveServer/ReorderPlaylistGroups", runtime.WithHTTPPathPattern("/api/playlists/{playlistID}/groups/order"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_EveningDetectiveServer_ReorderPlaylistGroups_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_EveningDetectiveServer_ReorderPlaylistGroups_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodPut, pattern_EveningDetectiveServer_SetPlaylistTrackGroup_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/crabs.evening_detective_server.EveningDetectiveServer/SetPlaylistTrackGroup", runtime.WithHTTPPathPattern("/api/playlists/{playlistID}/tracks/{trackID}/group"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_EveningDetectiveServer_SetPlaylistTrackGroup_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_EveningDetectiveServer_SetPlaylistTrackGroup_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_EveningDetectiveServer_GetPlaylists_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { mux.Handle(http.MethodGet, pattern_EveningDetectiveServer_GetPlaylists_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context()) ctx, cancel := context.WithCancel(req.Context())
defer cancel() defer cancel()
@@ -7442,6 +7728,74 @@ func RegisterEveningDetectiveServerHandlerClient(ctx context.Context, mux *runti
} }
forward_EveningDetectiveServer_DownloadFile_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) forward_EveningDetectiveServer_DownloadFile_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
}) })
mux.Handle(http.MethodPost, pattern_EveningDetectiveServer_SavePlaylistGroup_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/crabs.evening_detective_server.EveningDetectiveServer/SavePlaylistGroup", runtime.WithHTTPPathPattern("/api/playlists/{playlistID}/groups"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_EveningDetectiveServer_SavePlaylistGroup_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_EveningDetectiveServer_SavePlaylistGroup_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodDelete, pattern_EveningDetectiveServer_DeletePlaylistGroup_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/crabs.evening_detective_server.EveningDetectiveServer/DeletePlaylistGroup", runtime.WithHTTPPathPattern("/api/playlists/{playlistID}/groups/{groupID}"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_EveningDetectiveServer_DeletePlaylistGroup_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_EveningDetectiveServer_DeletePlaylistGroup_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodPut, pattern_EveningDetectiveServer_ReorderPlaylistGroups_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/crabs.evening_detective_server.EveningDetectiveServer/ReorderPlaylistGroups", runtime.WithHTTPPathPattern("/api/playlists/{playlistID}/groups/order"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_EveningDetectiveServer_ReorderPlaylistGroups_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_EveningDetectiveServer_ReorderPlaylistGroups_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodPut, pattern_EveningDetectiveServer_SetPlaylistTrackGroup_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/crabs.evening_detective_server.EveningDetectiveServer/SetPlaylistTrackGroup", runtime.WithHTTPPathPattern("/api/playlists/{playlistID}/tracks/{trackID}/group"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_EveningDetectiveServer_SetPlaylistTrackGroup_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_EveningDetectiveServer_SetPlaylistTrackGroup_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodGet, pattern_EveningDetectiveServer_GetPlaylists_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { mux.Handle(http.MethodGet, pattern_EveningDetectiveServer_GetPlaylists_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context()) ctx, cancel := context.WithCancel(req.Context())
defer cancel() defer cancel()
@@ -8971,6 +9325,10 @@ var (
pattern_EveningDetectiveServer_GetPermissions_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "ui", "permissions"}, "")) pattern_EveningDetectiveServer_GetPermissions_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "ui", "permissions"}, ""))
pattern_EveningDetectiveServer_UploadFile_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "files", "upload"}, "")) pattern_EveningDetectiveServer_UploadFile_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "files", "upload"}, ""))
pattern_EveningDetectiveServer_DownloadFile_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"api", "files", "filename"}, "")) pattern_EveningDetectiveServer_DownloadFile_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"api", "files", "filename"}, ""))
pattern_EveningDetectiveServer_SavePlaylistGroup_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3}, []string{"api", "playlists", "playlistID", "groups"}, ""))
pattern_EveningDetectiveServer_DeletePlaylistGroup_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3, 1, 0, 4, 1, 5, 4}, []string{"api", "playlists", "playlistID", "groups", "groupID"}, ""))
pattern_EveningDetectiveServer_ReorderPlaylistGroups_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3, 2, 4}, []string{"api", "playlists", "playlistID", "groups", "order"}, ""))
pattern_EveningDetectiveServer_SetPlaylistTrackGroup_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3, 1, 0, 4, 1, 5, 4, 2, 5}, []string{"api", "playlists", "playlistID", "tracks", "trackID", "group"}, ""))
pattern_EveningDetectiveServer_GetPlaylists_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "playlists"}, "")) pattern_EveningDetectiveServer_GetPlaylists_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "playlists"}, ""))
pattern_EveningDetectiveServer_CreatePlaylist_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "playlists"}, "")) pattern_EveningDetectiveServer_CreatePlaylist_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "playlists"}, ""))
pattern_EveningDetectiveServer_UpdatePlaylist_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"api", "playlists", "ID"}, "")) pattern_EveningDetectiveServer_UpdatePlaylist_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"api", "playlists", "ID"}, ""))
@@ -9091,6 +9449,10 @@ var (
forward_EveningDetectiveServer_GetPermissions_0 = runtime.ForwardResponseMessage forward_EveningDetectiveServer_GetPermissions_0 = runtime.ForwardResponseMessage
forward_EveningDetectiveServer_UploadFile_0 = runtime.ForwardResponseMessage forward_EveningDetectiveServer_UploadFile_0 = runtime.ForwardResponseMessage
forward_EveningDetectiveServer_DownloadFile_0 = runtime.ForwardResponseMessage forward_EveningDetectiveServer_DownloadFile_0 = runtime.ForwardResponseMessage
forward_EveningDetectiveServer_SavePlaylistGroup_0 = runtime.ForwardResponseMessage
forward_EveningDetectiveServer_DeletePlaylistGroup_0 = runtime.ForwardResponseMessage
forward_EveningDetectiveServer_ReorderPlaylistGroups_0 = runtime.ForwardResponseMessage
forward_EveningDetectiveServer_SetPlaylistTrackGroup_0 = runtime.ForwardResponseMessage
forward_EveningDetectiveServer_GetPlaylists_0 = runtime.ForwardResponseMessage forward_EveningDetectiveServer_GetPlaylists_0 = runtime.ForwardResponseMessage
forward_EveningDetectiveServer_CreatePlaylist_0 = runtime.ForwardResponseMessage forward_EveningDetectiveServer_CreatePlaylist_0 = runtime.ForwardResponseMessage
forward_EveningDetectiveServer_UpdatePlaylist_0 = runtime.ForwardResponseMessage forward_EveningDetectiveServer_UpdatePlaylist_0 = runtime.ForwardResponseMessage
+152
View File
@@ -55,6 +55,10 @@ const (
EveningDetectiveServer_GetPermissions_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/GetPermissions" EveningDetectiveServer_GetPermissions_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/GetPermissions"
EveningDetectiveServer_UploadFile_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/UploadFile" EveningDetectiveServer_UploadFile_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/UploadFile"
EveningDetectiveServer_DownloadFile_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/DownloadFile" EveningDetectiveServer_DownloadFile_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/DownloadFile"
EveningDetectiveServer_SavePlaylistGroup_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/SavePlaylistGroup"
EveningDetectiveServer_DeletePlaylistGroup_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/DeletePlaylistGroup"
EveningDetectiveServer_ReorderPlaylistGroups_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/ReorderPlaylistGroups"
EveningDetectiveServer_SetPlaylistTrackGroup_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/SetPlaylistTrackGroup"
EveningDetectiveServer_GetPlaylists_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/GetPlaylists" EveningDetectiveServer_GetPlaylists_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/GetPlaylists"
EveningDetectiveServer_CreatePlaylist_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/CreatePlaylist" EveningDetectiveServer_CreatePlaylist_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/CreatePlaylist"
EveningDetectiveServer_UpdatePlaylist_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/UpdatePlaylist" EveningDetectiveServer_UpdatePlaylist_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/UpdatePlaylist"
@@ -180,6 +184,10 @@ type EveningDetectiveServerClient interface {
GetPermissions(ctx context.Context, in *GetPermissionsReq, opts ...grpc.CallOption) (*GetPermissionsRsp, error) GetPermissions(ctx context.Context, in *GetPermissionsReq, opts ...grpc.CallOption) (*GetPermissionsRsp, error)
UploadFile(ctx context.Context, in *UploadFileReq, opts ...grpc.CallOption) (*UploadFileRsp, error) UploadFile(ctx context.Context, in *UploadFileReq, opts ...grpc.CallOption) (*UploadFileRsp, error)
DownloadFile(ctx context.Context, in *DownloadFileReq, opts ...grpc.CallOption) (*httpbody.HttpBody, error) DownloadFile(ctx context.Context, in *DownloadFileReq, opts ...grpc.CallOption) (*httpbody.HttpBody, error)
SavePlaylistGroup(ctx context.Context, in *SavePlaylistGroupReq, opts ...grpc.CallOption) (*SavePlaylistGroupRsp, error)
DeletePlaylistGroup(ctx context.Context, in *DeletePlaylistGroupReq, opts ...grpc.CallOption) (*DeletePlaylistGroupRsp, error)
ReorderPlaylistGroups(ctx context.Context, in *ReorderPlaylistGroupsReq, opts ...grpc.CallOption) (*ReorderPlaylistGroupsRsp, error)
SetPlaylistTrackGroup(ctx context.Context, in *SetPlaylistTrackGroupReq, opts ...grpc.CallOption) (*SetPlaylistTrackGroupRsp, error)
GetPlaylists(ctx context.Context, in *GetPlaylistsReq, opts ...grpc.CallOption) (*GetPlaylistsRsp, error) GetPlaylists(ctx context.Context, in *GetPlaylistsReq, opts ...grpc.CallOption) (*GetPlaylistsRsp, error)
CreatePlaylist(ctx context.Context, in *CreatePlaylistReq, opts ...grpc.CallOption) (*CreatePlaylistRsp, error) CreatePlaylist(ctx context.Context, in *CreatePlaylistReq, opts ...grpc.CallOption) (*CreatePlaylistRsp, error)
UpdatePlaylist(ctx context.Context, in *UpdatePlaylistReq, opts ...grpc.CallOption) (*UpdatePlaylistRsp, error) UpdatePlaylist(ctx context.Context, in *UpdatePlaylistReq, opts ...grpc.CallOption) (*UpdatePlaylistRsp, error)
@@ -580,6 +588,46 @@ func (c *eveningDetectiveServerClient) DownloadFile(ctx context.Context, in *Dow
return out, nil return out, nil
} }
func (c *eveningDetectiveServerClient) SavePlaylistGroup(ctx context.Context, in *SavePlaylistGroupReq, opts ...grpc.CallOption) (*SavePlaylistGroupRsp, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SavePlaylistGroupRsp)
err := c.cc.Invoke(ctx, EveningDetectiveServer_SavePlaylistGroup_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *eveningDetectiveServerClient) DeletePlaylistGroup(ctx context.Context, in *DeletePlaylistGroupReq, opts ...grpc.CallOption) (*DeletePlaylistGroupRsp, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(DeletePlaylistGroupRsp)
err := c.cc.Invoke(ctx, EveningDetectiveServer_DeletePlaylistGroup_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *eveningDetectiveServerClient) ReorderPlaylistGroups(ctx context.Context, in *ReorderPlaylistGroupsReq, opts ...grpc.CallOption) (*ReorderPlaylistGroupsRsp, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ReorderPlaylistGroupsRsp)
err := c.cc.Invoke(ctx, EveningDetectiveServer_ReorderPlaylistGroups_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *eveningDetectiveServerClient) SetPlaylistTrackGroup(ctx context.Context, in *SetPlaylistTrackGroupReq, opts ...grpc.CallOption) (*SetPlaylistTrackGroupRsp, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SetPlaylistTrackGroupRsp)
err := c.cc.Invoke(ctx, EveningDetectiveServer_SetPlaylistTrackGroup_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *eveningDetectiveServerClient) GetPlaylists(ctx context.Context, in *GetPlaylistsReq, opts ...grpc.CallOption) (*GetPlaylistsRsp, error) { func (c *eveningDetectiveServerClient) GetPlaylists(ctx context.Context, in *GetPlaylistsReq, opts ...grpc.CallOption) (*GetPlaylistsRsp, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetPlaylistsRsp) out := new(GetPlaylistsRsp)
@@ -1495,6 +1543,10 @@ type EveningDetectiveServerServer interface {
GetPermissions(context.Context, *GetPermissionsReq) (*GetPermissionsRsp, error) GetPermissions(context.Context, *GetPermissionsReq) (*GetPermissionsRsp, error)
UploadFile(context.Context, *UploadFileReq) (*UploadFileRsp, error) UploadFile(context.Context, *UploadFileReq) (*UploadFileRsp, error)
DownloadFile(context.Context, *DownloadFileReq) (*httpbody.HttpBody, error) DownloadFile(context.Context, *DownloadFileReq) (*httpbody.HttpBody, error)
SavePlaylistGroup(context.Context, *SavePlaylistGroupReq) (*SavePlaylistGroupRsp, error)
DeletePlaylistGroup(context.Context, *DeletePlaylistGroupReq) (*DeletePlaylistGroupRsp, error)
ReorderPlaylistGroups(context.Context, *ReorderPlaylistGroupsReq) (*ReorderPlaylistGroupsRsp, error)
SetPlaylistTrackGroup(context.Context, *SetPlaylistTrackGroupReq) (*SetPlaylistTrackGroupRsp, error)
GetPlaylists(context.Context, *GetPlaylistsReq) (*GetPlaylistsRsp, error) GetPlaylists(context.Context, *GetPlaylistsReq) (*GetPlaylistsRsp, error)
CreatePlaylist(context.Context, *CreatePlaylistReq) (*CreatePlaylistRsp, error) CreatePlaylist(context.Context, *CreatePlaylistReq) (*CreatePlaylistRsp, error)
UpdatePlaylist(context.Context, *UpdatePlaylistReq) (*UpdatePlaylistRsp, error) UpdatePlaylist(context.Context, *UpdatePlaylistReq) (*UpdatePlaylistRsp, error)
@@ -1692,6 +1744,18 @@ func (UnimplementedEveningDetectiveServerServer) UploadFile(context.Context, *Up
func (UnimplementedEveningDetectiveServerServer) DownloadFile(context.Context, *DownloadFileReq) (*httpbody.HttpBody, error) { func (UnimplementedEveningDetectiveServerServer) DownloadFile(context.Context, *DownloadFileReq) (*httpbody.HttpBody, error) {
return nil, status.Error(codes.Unimplemented, "method DownloadFile not implemented") return nil, status.Error(codes.Unimplemented, "method DownloadFile not implemented")
} }
func (UnimplementedEveningDetectiveServerServer) SavePlaylistGroup(context.Context, *SavePlaylistGroupReq) (*SavePlaylistGroupRsp, error) {
return nil, status.Error(codes.Unimplemented, "method SavePlaylistGroup not implemented")
}
func (UnimplementedEveningDetectiveServerServer) DeletePlaylistGroup(context.Context, *DeletePlaylistGroupReq) (*DeletePlaylistGroupRsp, error) {
return nil, status.Error(codes.Unimplemented, "method DeletePlaylistGroup not implemented")
}
func (UnimplementedEveningDetectiveServerServer) ReorderPlaylistGroups(context.Context, *ReorderPlaylistGroupsReq) (*ReorderPlaylistGroupsRsp, error) {
return nil, status.Error(codes.Unimplemented, "method ReorderPlaylistGroups not implemented")
}
func (UnimplementedEveningDetectiveServerServer) SetPlaylistTrackGroup(context.Context, *SetPlaylistTrackGroupReq) (*SetPlaylistTrackGroupRsp, error) {
return nil, status.Error(codes.Unimplemented, "method SetPlaylistTrackGroup not implemented")
}
func (UnimplementedEveningDetectiveServerServer) GetPlaylists(context.Context, *GetPlaylistsReq) (*GetPlaylistsRsp, error) { func (UnimplementedEveningDetectiveServerServer) GetPlaylists(context.Context, *GetPlaylistsReq) (*GetPlaylistsRsp, error) {
return nil, status.Error(codes.Unimplemented, "method GetPlaylists not implemented") return nil, status.Error(codes.Unimplemented, "method GetPlaylists not implemented")
} }
@@ -2500,6 +2564,78 @@ func _EveningDetectiveServer_DownloadFile_Handler(srv interface{}, ctx context.C
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _EveningDetectiveServer_SavePlaylistGroup_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SavePlaylistGroupReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(EveningDetectiveServerServer).SavePlaylistGroup(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: EveningDetectiveServer_SavePlaylistGroup_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(EveningDetectiveServerServer).SavePlaylistGroup(ctx, req.(*SavePlaylistGroupReq))
}
return interceptor(ctx, in, info, handler)
}
func _EveningDetectiveServer_DeletePlaylistGroup_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DeletePlaylistGroupReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(EveningDetectiveServerServer).DeletePlaylistGroup(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: EveningDetectiveServer_DeletePlaylistGroup_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(EveningDetectiveServerServer).DeletePlaylistGroup(ctx, req.(*DeletePlaylistGroupReq))
}
return interceptor(ctx, in, info, handler)
}
func _EveningDetectiveServer_ReorderPlaylistGroups_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ReorderPlaylistGroupsReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(EveningDetectiveServerServer).ReorderPlaylistGroups(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: EveningDetectiveServer_ReorderPlaylistGroups_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(EveningDetectiveServerServer).ReorderPlaylistGroups(ctx, req.(*ReorderPlaylistGroupsReq))
}
return interceptor(ctx, in, info, handler)
}
func _EveningDetectiveServer_SetPlaylistTrackGroup_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SetPlaylistTrackGroupReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(EveningDetectiveServerServer).SetPlaylistTrackGroup(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: EveningDetectiveServer_SetPlaylistTrackGroup_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(EveningDetectiveServerServer).SetPlaylistTrackGroup(ctx, req.(*SetPlaylistTrackGroupReq))
}
return interceptor(ctx, in, info, handler)
}
func _EveningDetectiveServer_GetPlaylists_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _EveningDetectiveServer_GetPlaylists_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetPlaylistsReq) in := new(GetPlaylistsReq)
if err := dec(in); err != nil { if err := dec(in); err != nil {
@@ -4207,6 +4343,22 @@ var EveningDetectiveServer_ServiceDesc = grpc.ServiceDesc{
MethodName: "DownloadFile", MethodName: "DownloadFile",
Handler: _EveningDetectiveServer_DownloadFile_Handler, Handler: _EveningDetectiveServer_DownloadFile_Handler,
}, },
{
MethodName: "SavePlaylistGroup",
Handler: _EveningDetectiveServer_SavePlaylistGroup_Handler,
},
{
MethodName: "DeletePlaylistGroup",
Handler: _EveningDetectiveServer_DeletePlaylistGroup_Handler,
},
{
MethodName: "ReorderPlaylistGroups",
Handler: _EveningDetectiveServer_ReorderPlaylistGroups_Handler,
},
{
MethodName: "SetPlaylistTrackGroup",
Handler: _EveningDetectiveServer_SetPlaylistTrackGroup_Handler,
},
{ {
MethodName: "GetPlaylists", MethodName: "GetPlaylists",
Handler: _EveningDetectiveServer_GetPlaylists_Handler, Handler: _EveningDetectiveServer_GetPlaylists_Handler,