generated from VLADIMIR/template
up
This commit is contained in:
+102
-2
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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, "не удалось изменить музыку игры")
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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]}
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user