generated from VLADIMIR/template
up
This commit is contained in:
@@ -382,6 +382,14 @@ service EveningDetectiveServer {
|
||||
};
|
||||
}
|
||||
|
||||
rpc ReorderPlaylistTracks(ReorderPlaylistTracksReq) returns (ReorderPlaylistTracksRsp) {
|
||||
option (google.api.http) = { put: "/api/playlists/{playlistID}/tracks/order" body: "*" };
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
tags : "Музыка";
|
||||
summary: "Изменить порядок треков";
|
||||
};
|
||||
}
|
||||
|
||||
rpc DeletePlaylistTrack(DeletePlaylistTrackReq) returns (DeletePlaylistTrackRsp) {
|
||||
option (google.api.http) = { delete: "/api/playlists/{playlistID}/tracks/{trackID}" };
|
||||
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
|
||||
@@ -1441,6 +1449,15 @@ message UploadPlaylistTrackRsp {
|
||||
Playlist playlist = 1;
|
||||
}
|
||||
|
||||
message ReorderPlaylistTracksReq {
|
||||
int32 playlistID = 1;
|
||||
repeated int32 trackIDs = 2;
|
||||
}
|
||||
|
||||
message ReorderPlaylistTracksRsp {
|
||||
Playlist playlist = 1;
|
||||
}
|
||||
|
||||
message DeletePlaylistTrackReq {
|
||||
int32 playlistID = 1;
|
||||
int32 trackID = 2;
|
||||
@@ -1892,6 +1909,7 @@ message BindScreenReq {
|
||||
string screenID = 1;
|
||||
string accessToken = 2;
|
||||
int32 gameID = 3;
|
||||
bool initializeAudio = 4;
|
||||
}
|
||||
|
||||
message BindScreenRsp {
|
||||
|
||||
@@ -2054,6 +2054,46 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/playlists/{playlistID}/tracks/order": {
|
||||
"put": {
|
||||
"summary": "Изменить порядок треков",
|
||||
"operationId": "EveningDetectiveServer_ReorderPlaylistTracks",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A successful response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/evening_detective_serverReorderPlaylistTracksRsp"
|
||||
}
|
||||
},
|
||||
"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/EveningDetectiveServerReorderPlaylistTracksBody"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Музыка"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/playlists/{playlistID}/tracks/{trackID}": {
|
||||
"delete": {
|
||||
"summary": "Удалить MP3 из плейлиста",
|
||||
@@ -4154,6 +4194,9 @@
|
||||
"gameID": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"initializeAudio": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -4247,6 +4290,18 @@
|
||||
"EveningDetectiveServerPublicScenarioBody": {
|
||||
"type": "object"
|
||||
},
|
||||
"EveningDetectiveServerReorderPlaylistTracksBody": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"trackIDs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"EveningDetectiveServerResetGameBody": {
|
||||
"type": "object"
|
||||
},
|
||||
@@ -5881,6 +5936,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"evening_detective_serverReorderPlaylistTracksRsp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"playlist": {
|
||||
"$ref": "#/definitions/evening_detective_serverPlaylist"
|
||||
}
|
||||
}
|
||||
},
|
||||
"evening_detective_serverResetGameLocalModeRsp": {
|
||||
"type": "object"
|
||||
},
|
||||
|
||||
@@ -18,6 +18,8 @@ func playlistErrorStatus(err error, fallback string) error {
|
||||
errors.Is(err, playlist_service.ErrGameNotFound),
|
||||
errors.Is(err, playlist_service.ErrScenarioNotFound):
|
||||
return status.Error(codes.NotFound, err.Error())
|
||||
case errors.Is(err, playlist_service.ErrTrackOrderChanged):
|
||||
return status.Error(codes.Aborted, err.Error())
|
||||
case errors.Is(err, playlist_service.ErrPlaylistInUse):
|
||||
return status.Error(codes.FailedPrecondition, err.Error())
|
||||
default:
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"evening_detective_server/internal/services/playlist_service"
|
||||
proto "evening_detective_server/proto"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func TestPlaylistOrderErrorCodes(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
err error
|
||||
code codes.Code
|
||||
}{
|
||||
{"invalid", playlist_service.ErrInvalidTrack, codes.InvalidArgument},
|
||||
{"conflict", playlist_service.ErrTrackOrderChanged, codes.Aborted},
|
||||
{"missing", playlist_service.ErrPlaylistNotFound, codes.NotFound},
|
||||
{"internal", errors.New("secret SQL"), codes.Internal},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := playlistErrorStatus(tc.err, "Не удалось сохранить порядок")
|
||||
if status.Code(err) != tc.code {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if tc.code == codes.Internal && status.Convert(err).Message() != "Не удалось сохранить порядок" {
|
||||
t.Fatal("internal details exposed")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlaylistOrderRequiresAuthentication(t *testing.T) {
|
||||
for _, ctx := range []context.Context{context.Background()} {
|
||||
t.Run("anonymous", func(t *testing.T) {
|
||||
_, err := (&server{}).ReorderPlaylistTracks(ctx, &proto.ReorderPlaylistTracksReq{PlaylistID: 1, TrackIDs: []int32{1}})
|
||||
if status.Code(err) != codes.PermissionDenied {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -159,3 +159,18 @@ func (s *server) SetGameMusic(ctx context.Context, req *proto.SetGameMusicReq) (
|
||||
}
|
||||
return &proto.SetGameMusicRsp{State: mapMusicState(state)}, nil
|
||||
}
|
||||
|
||||
func (s *server) ReorderPlaylistTracks(ctx context.Context, req *proto.ReorderPlaylistTracksReq) (*proto.ReorderPlaylistTracksRsp, error) {
|
||||
if !canManagePlaylists(ctx) {
|
||||
return nil, status.Error(codes.PermissionDenied, "permission denied")
|
||||
}
|
||||
ids := make([]int, len(req.GetTrackIDs()))
|
||||
for i, id := range req.GetTrackIDs() {
|
||||
ids[i] = int(id)
|
||||
}
|
||||
item, err := s.playlistService.Reorder(ctx, int(req.GetPlaylistID()), ids)
|
||||
if err != nil {
|
||||
return nil, playlistErrorStatus(err, "не удалось сохранить порядок треков")
|
||||
}
|
||||
return &proto.ReorderPlaylistTracksRsp{Playlist: mapPlaylist(item)}, nil
|
||||
}
|
||||
|
||||
@@ -123,14 +123,17 @@ func (s *server) BindScreen(ctx context.Context, req *proto.BindScreenReq) (*pro
|
||||
return nil, status.Error(codes.NotFound, "игра не найдена")
|
||||
}
|
||||
hash := hashScreenToken(req.AccessToken)
|
||||
if err = s.screenService.Bind(ctx, id, int(req.GameID), hash); err != nil {
|
||||
isNewBinding, err := s.screenService.Bind(ctx, id, int(req.GameID), hash, req.GetInitializeAudio())
|
||||
if err != nil {
|
||||
if errors.Is(err, screen_service.ErrScreenNotFound) {
|
||||
return nil, status.Error(codes.Unauthenticated, "недействительная сессия экрана")
|
||||
}
|
||||
return nil, status.Error(codes.Internal, "не удалось привязать экран")
|
||||
}
|
||||
if err := s.screenPlanService.ApplyCurrent(ctx, int(req.GameID)); err != nil {
|
||||
return nil, status.Error(codes.Internal, "не удалось применить план экранов")
|
||||
if isNewBinding {
|
||||
if err := s.screenPlanService.ApplyCurrent(ctx, int(req.GameID), id); err != nil {
|
||||
return nil, status.Error(codes.Internal, "не удалось применить план экранов")
|
||||
}
|
||||
}
|
||||
session, err := s.screenService.Get(ctx, id, hash)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package playlists_repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"evening_detective_server/internal/repos/games_repo"
|
||||
"evening_detective_server/internal/repos/scenarios_repo"
|
||||
"evening_detective_server/internal/repos/screen_sessions_repo"
|
||||
"evening_detective_server/internal/repos/user_agreements_repo"
|
||||
"evening_detective_server/internal/repos/users_repo"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
type playlistFixture struct {
|
||||
pool *pgxpool.Pool
|
||||
repo *Repo
|
||||
screens *screen_sessions_repo.ScreenSessionsRepo
|
||||
gameID int
|
||||
}
|
||||
|
||||
// newPlaylistFixture uses real migrations in an isolated test schema; all game data uses repositories.
|
||||
func newPlaylistFixture(t *testing.T) playlistFixture {
|
||||
t.Helper()
|
||||
dsn := os.Getenv("TEST_DATABASE_URL")
|
||||
if dsn == "" {
|
||||
t.Skip("set TEST_DATABASE_URL for PostgreSQL playlist checks")
|
||||
}
|
||||
ctx := context.Background()
|
||||
admin, err := pgxpool.New(ctx, dsn)
|
||||
requireNoError(t, err)
|
||||
schema := fmt.Sprintf("playlist_test_%d", time.Now().UnixNano())
|
||||
quoted := pgx.Identifier{schema}.Sanitize()
|
||||
_, err = admin.Exec(ctx, "CREATE SCHEMA "+quoted)
|
||||
requireNoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
_, err := admin.Exec(ctx, "DROP SCHEMA "+quoted+" CASCADE")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
admin.Close()
|
||||
})
|
||||
config, err := pgxpool.ParseConfig(dsn)
|
||||
requireNoError(t, err)
|
||||
config.ConnConfig.RuntimeParams["search_path"] = schema
|
||||
config.ConnConfig.RuntimeParams["timezone"] = "UTC"
|
||||
pool, err := pgxpool.NewWithConfig(ctx, config)
|
||||
requireNoError(t, err)
|
||||
t.Cleanup(pool.Close)
|
||||
files, err := filepath.Glob("../../../migrations/*.sql")
|
||||
requireNoError(t, err)
|
||||
for _, name := range files {
|
||||
raw, err := os.ReadFile(name)
|
||||
requireNoError(t, err)
|
||||
_, err = pool.Exec(ctx, strings.Split(string(raw), "-- +goose Down")[0])
|
||||
if err != nil {
|
||||
t.Fatalf("migration %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
f := playlistFixture{pool: pool, repo: New(pool), screens: screen_sessions_repo.NewScreenSessionsRepo(pool)}
|
||||
users := users_repo.NewUserRepo(pool)
|
||||
scenarios := scenarios_repo.NewScenariosRepo(pool)
|
||||
games := games_repo.NewGamesRepo(pool)
|
||||
var userID, scenarioID int
|
||||
userID, err = users.AddUserWithAgreements(ctx, "organizer", "export@test.invalid", "hash", []string{"organizer"},
|
||||
[]user_agreements_repo.Agreement{{AgreementType: "terms", Version: "test"}})
|
||||
requireNoError(t, err)
|
||||
scenarioID, err = scenarios.AddScenario(ctx, "Original story", userID)
|
||||
requireNoError(t, err)
|
||||
requireNoError(t, scenarios.UpdateStoryByScenarioID(ctx, scenarioID,
|
||||
`{"introduction":{"text":"Intro"},"questions":[{"code":"q","text":"Who?","answer":"Butler"}]}`))
|
||||
f.gameID, err = games.AddGame(ctx, "Test export", "Original description", nil, time.Now().Add(12*time.Hour), int32(scenarioID), "offline")
|
||||
requireNoError(t, err)
|
||||
return f
|
||||
}
|
||||
|
||||
func requireNoError(t *testing.T, err error) {
|
||||
t.Helper()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package playlists_repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"slices"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
func lockPlaylist(ctx context.Context, tx pgx.Tx, id int) error {
|
||||
var found int
|
||||
err := tx.QueryRow(
|
||||
ctx,
|
||||
`SELECT id
|
||||
FROM playlists
|
||||
WHERE id = $1
|
||||
FOR UPDATE`,
|
||||
id,
|
||||
).Scan(&found)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return ErrPlaylistNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Reorder atomically changes order and preserves each game's current track.
|
||||
func (r *Repo) Reorder(ctx context.Context, playlistID int, trackIDs []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_tracks
|
||||
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(trackIDs)
|
||||
slices.Sort(sortedOld)
|
||||
slices.Sort(sortedNew)
|
||||
if !slices.Equal(sortedOld, sortedNew) {
|
||||
return nil, ErrTrackOrderChanged
|
||||
}
|
||||
if !slices.Equal(old, trackIDs) {
|
||||
// Lock state before changing indices; timing and playback remain untouched.
|
||||
_, err = tx.Exec(
|
||||
ctx,
|
||||
`UPDATE game_music_states ms
|
||||
SET track_index = COALESCE(array_position($3::int[], ($2::int[])[ms.track_index + 1]) - 1, 0),
|
||||
state_version = ms.state_version + 1,
|
||||
updated_at = NOW()
|
||||
FROM games g
|
||||
JOIN scenarios s ON s.id = g.scenario_id
|
||||
WHERE ms.game_id = g.id
|
||||
AND COALESCE(ms.playlist_id, g.playlist_id, s.playlist_id) = $1`,
|
||||
playlistID,
|
||||
old,
|
||||
trackIDs,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = tx.Exec(
|
||||
ctx,
|
||||
`UPDATE playlist_tracks t
|
||||
SET position = ordered.position - 1
|
||||
FROM unnest($2::int[]) WITH ORDINALITY AS ordered(id, position)
|
||||
WHERE t.playlist_id = $1 AND t.id = ordered.id`,
|
||||
playlistID,
|
||||
trackIDs,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
items, err := list(ctx, tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, item := range items {
|
||||
if item.ID == playlistID {
|
||||
return item, nil
|
||||
}
|
||||
}
|
||||
return nil, ErrPlaylistNotFound
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
package playlists_repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestReorderPreservesPlaybackAndRejectsChangedMembership(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.Get(ctx, p.ID)
|
||||
requireNoError(t, err)
|
||||
before, err := f.repo.SetMusic(ctx, f.gameID, p.ID, 1, 1234, 37, true, playback)
|
||||
requireNoError(t, err)
|
||||
if err := f.repo.Delete(ctx, p.ID); !errors.Is(err, ErrPlaylistInUse) {
|
||||
t.Fatalf("active playlist deletion: %v", err)
|
||||
}
|
||||
if _, err := f.repo.DeleteTrack(ctx, p.ID, p.Tracks[0].ID); !errors.Is(err, ErrPlaylistInUse) {
|
||||
t.Fatalf("active track deletion: %v", err)
|
||||
}
|
||||
order := []int{p.Tracks[1].ID, p.Tracks[2].ID, p.Tracks[0].ID}
|
||||
reordered, err := f.repo.Reorder(ctx, p.ID, order)
|
||||
requireNoError(t, err)
|
||||
if reordered.Tracks[0].ID != p.Tracks[1].ID {
|
||||
t.Fatal("order not saved")
|
||||
}
|
||||
after, err := f.repo.GetMusic(ctx, f.gameID)
|
||||
requireNoError(t, err)
|
||||
if after.TrackIndex != 0 || after.Playlist.Tracks[0].ID != before.Playlist.Tracks[1].ID || after.Playback != playback || after.Volume != 37 || !after.Loop || after.Version != before.Version+1 {
|
||||
t.Fatalf("transport changed: %+v", after)
|
||||
}
|
||||
if after.PositionMs < before.PositionMs || after.PositionMs-before.PositionMs > 1000 || !sameTime(before.StartedAt, after.StartedAt) {
|
||||
t.Fatal("playback position reset")
|
||||
}
|
||||
_, err = f.repo.Reorder(ctx, p.ID, order)
|
||||
requireNoError(t, err)
|
||||
unchanged, err := f.repo.GetMusic(ctx, f.gameID)
|
||||
requireNoError(t, err)
|
||||
if unchanged.Version != after.Version {
|
||||
t.Fatal("no-op changed version")
|
||||
}
|
||||
for _, invalid := range [][]int{order[:2], {order[0], order[0], order[2]}, {order[0], order[1], 999999}} {
|
||||
if _, err := f.repo.Reorder(ctx, p.ID, invalid); !errors.Is(err, ErrTrackOrderChanged) {
|
||||
t.Fatalf("expected membership conflict, got %v", err)
|
||||
}
|
||||
}
|
||||
current, err := f.repo.Get(ctx, p.ID)
|
||||
requireNoError(t, err)
|
||||
for i, track := range current.Tracks {
|
||||
if track.ID != order[i] {
|
||||
t.Fatal("failed reorder changed data")
|
||||
}
|
||||
}
|
||||
empty, err := f.repo.Create(ctx, "Empty")
|
||||
requireNoError(t, err)
|
||||
_, err = f.repo.Reorder(ctx, empty.ID, nil)
|
||||
requireNoError(t, err)
|
||||
if _, err := f.repo.Reorder(ctx, 999999, nil); !errors.Is(err, ErrPlaylistNotFound) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func sameTime(a, b *time.Time) bool {
|
||||
return a == nil && b == nil || a != nil && b != nil && a.Equal(*b)
|
||||
}
|
||||
|
||||
func TestConcurrentAdditionsKeepDistinctPositions(t *testing.T) {
|
||||
for _, count := range []int{8} {
|
||||
t.Run("parallel uploads", func(t *testing.T) {
|
||||
f := newPlaylistFixture(t)
|
||||
ctx := context.Background()
|
||||
p, err := f.repo.Create(ctx, "Concurrent")
|
||||
requireNoError(t, err)
|
||||
var wg sync.WaitGroup
|
||||
failures := make(chan error, count)
|
||||
for i := 0; i < count; i++ {
|
||||
wg.Go(func() { failures <- f.repo.AddTrack(ctx, p.ID, "Track", fmt.Sprintf("track-%d.mp3", i)) })
|
||||
}
|
||||
wg.Wait()
|
||||
close(failures)
|
||||
for err := range failures {
|
||||
requireNoError(t, err)
|
||||
}
|
||||
p, err = f.repo.Get(ctx, p.ID)
|
||||
requireNoError(t, err)
|
||||
positions := make([]int, len(p.Tracks))
|
||||
for i, track := range p.Tracks {
|
||||
positions[i] = track.Position
|
||||
}
|
||||
if !slices.Equal(positions, []int{0, 1, 2, 3, 4, 5, 6, 7}) {
|
||||
t.Fatal(positions)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBindInitializesAudioWithoutChangingTransport(t *testing.T) {
|
||||
for _, initialize := range []bool{false, true} {
|
||||
t.Run(map[bool]string{false: "legacy", true: "initialize"}[initialize], func(t *testing.T) {
|
||||
f := newPlaylistFixture(t)
|
||||
ctx := context.Background()
|
||||
first, err := f.screens.Create(ctx, "first")
|
||||
requireNoError(t, err)
|
||||
requireNoError(t, f.screens.Bind(ctx, first, f.gameID, "first"))
|
||||
requireNoError(t, f.screens.SetSound(ctx, first, true))
|
||||
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)
|
||||
requireNoError(t, err)
|
||||
second, err := f.screens.Create(ctx, "second")
|
||||
requireNoError(t, err)
|
||||
if _, err := f.screens.BindWithAudio(ctx, second, f.gameID, "wrong", initialize); err == nil {
|
||||
t.Fatal("bad token accepted")
|
||||
}
|
||||
isNewBinding, err := f.screens.BindWithAudio(ctx, second, f.gameID, "second", initialize)
|
||||
requireNoError(t, err)
|
||||
if !isNewBinding {
|
||||
t.Fatal("new binding not detected")
|
||||
}
|
||||
isNewBinding, err = f.screens.BindWithAudio(ctx, second, f.gameID, "second", initialize)
|
||||
requireNoError(t, err)
|
||||
if isNewBinding {
|
||||
t.Fatal("repeat binding would reset transport")
|
||||
}
|
||||
requireNoError(t, f.screens.InitializeScreen(ctx, second, f.gameID, 1, "", "Pause", 600))
|
||||
original, err := f.screens.Get(ctx, first, "first")
|
||||
requireNoError(t, err)
|
||||
joined, err := f.screens.Get(ctx, second, "second")
|
||||
requireNoError(t, err)
|
||||
after, err := f.repo.GetMusic(ctx, f.gameID)
|
||||
requireNoError(t, err)
|
||||
if original.Command != 2 || original.Position != 3456 || joined.Command != 2 || joined.Position != 3456 {
|
||||
t.Fatal("join reset display")
|
||||
}
|
||||
if original.Sound == initialize || joined.Sound != initialize {
|
||||
t.Fatal("incorrect sound assignment")
|
||||
}
|
||||
expectedVolume := int32(42)
|
||||
if initialize {
|
||||
expectedVolume = 100
|
||||
if joined.Volume != 100 {
|
||||
t.Fatal(joined.Volume)
|
||||
}
|
||||
}
|
||||
if after.Volume != expectedVolume || after.Playback != before.Playback || !sameTime(after.StartedAt, before.StartedAt) {
|
||||
t.Fatal("audio initialization changed playback")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReorderReadersSeeConsistentTrackAndIndex(t *testing.T) {
|
||||
for _, iterations := range []int{20} {
|
||||
t.Run("concurrent snapshots", func(t *testing.T) {
|
||||
f := newPlaylistFixture(t)
|
||||
ctx := context.Background()
|
||||
p, err := f.repo.Create(ctx, "Snapshot")
|
||||
requireNoError(t, err)
|
||||
for _, name := range []string{"a", "b", "c"} {
|
||||
requireNoError(t, f.repo.AddTrack(ctx, p.ID, name, name+".mp3"))
|
||||
}
|
||||
p, err = f.repo.Get(ctx, p.ID)
|
||||
requireNoError(t, err)
|
||||
_, err = f.repo.SetMusic(ctx, f.gameID, p.ID, 1, 1000, 50, true, 3)
|
||||
requireNoError(t, err)
|
||||
old := []int{p.Tracks[0].ID, p.Tracks[1].ID, p.Tracks[2].ID}
|
||||
reordered := []int{old[1], old[2], old[0]}
|
||||
failures := make(chan error, 1)
|
||||
go func() {
|
||||
for i := 0; i < iterations; i++ {
|
||||
order := old
|
||||
if i%2 == 0 {
|
||||
order = reordered
|
||||
}
|
||||
if _, err := f.repo.Reorder(ctx, p.ID, order); err != nil {
|
||||
failures <- err
|
||||
return
|
||||
}
|
||||
}
|
||||
failures <- nil
|
||||
}()
|
||||
for i := 0; i < iterations; i++ {
|
||||
state, err := f.repo.GetMusic(ctx, f.gameID)
|
||||
requireNoError(t, err)
|
||||
if state.Playlist.Tracks[state.TrackIndex].ID != old[1] {
|
||||
t.Fatal("mixed playlist order and current index")
|
||||
}
|
||||
}
|
||||
requireNoError(t, <-failures)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcurrentBindingsAssignSoundOnce(t *testing.T) {
|
||||
for _, count := range []int{2} {
|
||||
t.Run("two screens", func(t *testing.T) {
|
||||
f := newPlaylistFixture(t)
|
||||
ctx := context.Background()
|
||||
ids := make([]int, count)
|
||||
for i := range ids {
|
||||
var err error
|
||||
ids[i], err = f.screens.Create(ctx, fmt.Sprintf("token-%d", i))
|
||||
requireNoError(t, err)
|
||||
}
|
||||
failures := make(chan error, count)
|
||||
var wg sync.WaitGroup
|
||||
for i, id := range ids {
|
||||
wg.Go(func() {
|
||||
_, err := f.screens.BindWithAudio(ctx, id, f.gameID, fmt.Sprintf("token-%d", i), true)
|
||||
failures <- err
|
||||
})
|
||||
}
|
||||
wg.Wait()
|
||||
close(failures)
|
||||
for err := range failures {
|
||||
requireNoError(t, err)
|
||||
}
|
||||
screens, err := f.screens.ListByGame(ctx, f.gameID)
|
||||
requireNoError(t, err)
|
||||
sound := 0
|
||||
for _, s := range screens {
|
||||
if s.Sound {
|
||||
sound++
|
||||
}
|
||||
if s.Volume != 100 {
|
||||
t.Fatal(s.Volume)
|
||||
}
|
||||
}
|
||||
if sound != 1 {
|
||||
t.Fatalf("%d screens have sound", sound)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -10,11 +10,12 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrPlaylistNotFound = errors.New("плейлист не найден")
|
||||
ErrPlaylistInUse = errors.New("плейлист используется")
|
||||
ErrTrackNotFound = errors.New("трек не найден")
|
||||
ErrGameNotFound = errors.New("игра не найдена")
|
||||
ErrScenarioNotFound = errors.New("сценарий не найден")
|
||||
ErrPlaylistNotFound = errors.New("плейлист не найден")
|
||||
ErrTrackOrderChanged = errors.New("состав треков изменился; обновите список")
|
||||
ErrPlaylistInUse = errors.New("плейлист используется")
|
||||
ErrTrackNotFound = errors.New("трек не найден")
|
||||
ErrGameNotFound = errors.New("игра не найдена")
|
||||
ErrScenarioNotFound = errors.New("сценарий не найден")
|
||||
)
|
||||
|
||||
// Track describes a stored playlist track.
|
||||
@@ -54,11 +55,28 @@ func New(pool *pgxpool.Pool) *Repo { return &Repo{pool: pool} }
|
||||
|
||||
// List returns all playlists with their tracks.
|
||||
func (r *Repo) List(ctx context.Context) ([]*Playlist, error) {
|
||||
rows, err := r.pool.Query(ctx, `
|
||||
SELECT p.id, p.name, t.id, t.name, t.filename, t.position
|
||||
return list(ctx, r.pool)
|
||||
}
|
||||
|
||||
// IPlaylistReader reads catalogues from a pool or a consistent transaction snapshot.
|
||||
type IPlaylistReader interface {
|
||||
Query(context.Context, string, ...any) (pgx.Rows, error)
|
||||
}
|
||||
|
||||
func list(ctx context.Context, reader IPlaylistReader) ([]*Playlist, error) {
|
||||
rows, err := reader.Query(
|
||||
ctx,
|
||||
`SELECT
|
||||
p.id,
|
||||
p.name,
|
||||
t.id,
|
||||
t.name,
|
||||
t.filename,
|
||||
t.position
|
||||
FROM playlists p
|
||||
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`,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -136,68 +154,128 @@ func (r *Repo) Update(ctx context.Context, id int, name string) (*Playlist, erro
|
||||
|
||||
// Delete removes a playlist that is not assigned to a scenario or game.
|
||||
func (r *Repo) Delete(ctx context.Context, id int) error {
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
if err := lockPlaylist(ctx, tx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
var used bool
|
||||
if err := r.pool.QueryRow(ctx, `
|
||||
if err := tx.QueryRow(
|
||||
ctx,
|
||||
`
|
||||
SELECT EXISTS (SELECT 1 FROM scenarios WHERE playlist_id = $1)
|
||||
OR EXISTS (SELECT 1 FROM games WHERE playlist_id = $1)
|
||||
`, id).Scan(&used); err != nil {
|
||||
OR EXISTS (SELECT 1 FROM game_music_states WHERE playlist_id = $1)
|
||||
`,
|
||||
id,
|
||||
).Scan(&used); err != nil {
|
||||
return err
|
||||
}
|
||||
if used {
|
||||
return ErrPlaylistInUse
|
||||
}
|
||||
result, err := r.pool.Exec(ctx, `
|
||||
result, err := tx.Exec(
|
||||
ctx,
|
||||
`
|
||||
DELETE FROM playlists
|
||||
WHERE id = $1
|
||||
`, id)
|
||||
`,
|
||||
id,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.RowsAffected() == 0 {
|
||||
return ErrPlaylistNotFound
|
||||
}
|
||||
return nil
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
// AddTrack adds a track to a playlist.
|
||||
func (r *Repo) AddTrack(ctx context.Context, playlistID int, name, filename string) error {
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
if err := lockPlaylist(ctx, tx, playlistID); err != nil {
|
||||
return err
|
||||
}
|
||||
var position int
|
||||
if err := r.pool.QueryRow(ctx, `
|
||||
if err := tx.QueryRow(
|
||||
ctx,
|
||||
`
|
||||
SELECT COALESCE(MAX(position), -1) + 1
|
||||
FROM playlist_tracks
|
||||
WHERE playlist_id = $1
|
||||
`, playlistID).Scan(&position); err != nil {
|
||||
`,
|
||||
playlistID,
|
||||
).Scan(&position); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := r.pool.Exec(ctx, `
|
||||
_, err = tx.Exec(
|
||||
ctx,
|
||||
`
|
||||
INSERT INTO playlist_tracks (playlist_id, name, filename, position)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
`, playlistID, name, filename, position)
|
||||
return err
|
||||
`,
|
||||
playlistID,
|
||||
name,
|
||||
filename,
|
||||
position,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
// DeleteTrack removes a track and returns its stored filename.
|
||||
func (r *Repo) DeleteTrack(ctx context.Context, playlistID, trackID int) (string, error) {
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
if err := lockPlaylist(ctx, tx, playlistID); err != nil {
|
||||
return "", err
|
||||
}
|
||||
var filename string
|
||||
var used bool
|
||||
if err := r.pool.QueryRow(ctx, `
|
||||
if err = tx.QueryRow(
|
||||
ctx,
|
||||
`
|
||||
SELECT EXISTS (SELECT 1 FROM scenarios WHERE playlist_id = $1)
|
||||
OR EXISTS (SELECT 1 FROM games WHERE playlist_id = $1)
|
||||
`, playlistID).Scan(&used); err != nil {
|
||||
OR EXISTS (SELECT 1 FROM game_music_states WHERE playlist_id = $1)
|
||||
`,
|
||||
playlistID,
|
||||
).Scan(&used); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if used {
|
||||
return "", ErrPlaylistInUse
|
||||
}
|
||||
err := r.pool.QueryRow(ctx, `
|
||||
err = tx.QueryRow(
|
||||
ctx,
|
||||
`
|
||||
DELETE FROM playlist_tracks
|
||||
WHERE id = $1 AND playlist_id = $2
|
||||
RETURNING filename
|
||||
`, trackID, playlistID).Scan(&filename)
|
||||
`,
|
||||
trackID,
|
||||
playlistID,
|
||||
).Scan(&filename)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return "", ErrTrackNotFound
|
||||
}
|
||||
return filename, err
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filename, tx.Commit(ctx)
|
||||
}
|
||||
|
||||
// SetScenario assigns a playlist to a scenario.
|
||||
@@ -244,9 +322,30 @@ func (r *Repo) SetGame(ctx context.Context, gameID, playlistID int) error {
|
||||
|
||||
// GetMusic returns the effective and current music state for a game.
|
||||
func (r *Repo) GetMusic(ctx context.Context, gameID int) (*MusicState, error) {
|
||||
tx, err := r.pool.BeginTx(ctx, pgx.TxOptions{IsoLevel: pgx.RepeatableRead, AccessMode: pgx.ReadOnly})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
playlists, err := list(ctx, tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
get := func(id int) (*Playlist, error) {
|
||||
for _, playlist := range playlists {
|
||||
if playlist.ID == id {
|
||||
return playlist, nil
|
||||
}
|
||||
}
|
||||
return nil, ErrPlaylistNotFound
|
||||
}
|
||||
var gamePlaylistID, scenarioPlaylistID *int
|
||||
err := r.pool.QueryRow(ctx, `
|
||||
SELECT g.playlist_id, s.playlist_id
|
||||
err = tx.QueryRow(
|
||||
ctx,
|
||||
`
|
||||
SELECT
|
||||
g.playlist_id,
|
||||
s.playlist_id
|
||||
FROM games g
|
||||
JOIN scenarios s ON s.id = g.scenario_id
|
||||
WHERE g.id = $1
|
||||
@@ -265,18 +364,30 @@ func (r *Repo) GetMusic(ctx context.Context, gameID int) (*MusicState, error) {
|
||||
}
|
||||
state := &MusicState{ScenarioID: scenarioPlaylistID, GameID: gamePlaylistID, Volume: 100, Playback: 1}
|
||||
if playlistID != nil {
|
||||
state.Playlist, err = r.Get(ctx, *playlistID)
|
||||
state.Playlist, err = get(*playlistID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var trackIndex int
|
||||
var statePlaylistID *int
|
||||
err = r.pool.QueryRow(ctx, `
|
||||
SELECT playlist_id, track_index, position_ms, volume, loop, playback, state_version, started_at
|
||||
err = tx.QueryRow(
|
||||
ctx,
|
||||
`
|
||||
SELECT
|
||||
playlist_id,
|
||||
track_index,
|
||||
position_ms,
|
||||
volume,
|
||||
loop,
|
||||
playback,
|
||||
state_version,
|
||||
started_at
|
||||
FROM game_music_states
|
||||
WHERE game_id = $1
|
||||
`, gameID).
|
||||
`,
|
||||
gameID,
|
||||
).
|
||||
Scan(&statePlaylistID, &trackIndex, &state.PositionMs, &state.Volume, &state.Loop, &state.Playback, &state.Version, &state.StartedAt)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
state.Version = 1
|
||||
@@ -290,7 +401,7 @@ func (r *Repo) GetMusic(ctx context.Context, gameID int) (*MusicState, error) {
|
||||
state.PositionMs += time.Since(*state.StartedAt).Milliseconds()
|
||||
}
|
||||
if statePlaylistID != nil && (state.Playlist == nil || state.Playlist.ID != *statePlaylistID) {
|
||||
state.Playlist, err = r.Get(ctx, *statePlaylistID)
|
||||
state.Playlist, err = get(*statePlaylistID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -300,12 +411,19 @@ func (r *Repo) GetMusic(ctx context.Context, gameID int) (*MusicState, error) {
|
||||
|
||||
// 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) {
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
if playlistID > 0 {
|
||||
if _, err := r.Get(ctx, playlistID); err != nil {
|
||||
if err := lockPlaylist(ctx, tx, playlistID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
_, err := r.pool.Exec(ctx, `
|
||||
_, err = tx.Exec(
|
||||
ctx,
|
||||
`
|
||||
INSERT INTO game_music_states (
|
||||
game_id, playlist_id, track_index, position_ms, volume, loop,
|
||||
playback, state_version, started_at
|
||||
@@ -336,5 +454,8 @@ func (r *Repo) SetMusic(ctx context.Context, gameID, playlistID, trackIndex int,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.GetMusic(ctx, gameID)
|
||||
}
|
||||
|
||||
@@ -198,17 +198,84 @@ func (r *ScreenSessionsRepo) SetTimerCommand(ctx context.Context, gameID int, co
|
||||
|
||||
// Bind attaches a screen session to a game.
|
||||
func (r *ScreenSessionsRepo) Bind(ctx context.Context, id, gameID int, tokenHash string) error {
|
||||
result, err := r.pool.Exec(ctx, `
|
||||
UPDATE screen_sessions
|
||||
SET game_id = $1, updated_at = NOW()
|
||||
WHERE id = $2 AND access_token_hash = $3`, gameID, id, tokenHash)
|
||||
_, err := r.BindWithAudio(ctx, id, gameID, tokenHash, false)
|
||||
return err
|
||||
}
|
||||
|
||||
// BindWithAudio initializes audio without transport commands and reports a new game binding.
|
||||
func (r *ScreenSessionsRepo) BindWithAudio(ctx context.Context, id, gameID int, tokenHash string, initializeAudio bool) (bool, error) {
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
if _, err := tx.Exec(
|
||||
ctx,
|
||||
`SELECT id
|
||||
FROM games
|
||||
WHERE id = $1
|
||||
FOR UPDATE`,
|
||||
gameID,
|
||||
); err != nil {
|
||||
return false, err
|
||||
}
|
||||
var previousGameID *int
|
||||
if err := tx.QueryRow(
|
||||
ctx,
|
||||
`SELECT game_id
|
||||
FROM screen_sessions
|
||||
WHERE id = $1 AND access_token_hash = $2
|
||||
FOR UPDATE`,
|
||||
id,
|
||||
tokenHash,
|
||||
).Scan(&previousGameID); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return false, ErrScreenNotFound
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
result, err := tx.Exec(
|
||||
ctx,
|
||||
`UPDATE screen_sessions
|
||||
SET game_id = $1,
|
||||
sound_enabled = CASE WHEN $4 THEN TRUE ELSE sound_enabled END,
|
||||
volume = CASE WHEN $4 THEN 100 ELSE volume END,
|
||||
state_version = state_version + 1,
|
||||
updated_at = NOW()
|
||||
WHERE id = $2 AND access_token_hash = $3`,
|
||||
gameID,
|
||||
id,
|
||||
tokenHash,
|
||||
initializeAudio,
|
||||
)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if result.RowsAffected() == 0 {
|
||||
return ErrScreenNotFound
|
||||
return false, ErrScreenNotFound
|
||||
}
|
||||
return nil
|
||||
if initializeAudio {
|
||||
if _, err := tx.Exec(
|
||||
ctx,
|
||||
`UPDATE screen_sessions
|
||||
SET sound_enabled = FALSE, updated_at = NOW()
|
||||
WHERE game_id = $1 AND id <> $2`,
|
||||
gameID,
|
||||
id,
|
||||
); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if _, err := tx.Exec(
|
||||
ctx,
|
||||
`UPDATE game_music_states
|
||||
SET volume = 100, state_version = state_version + 1, updated_at = NOW()
|
||||
WHERE game_id = $1`,
|
||||
gameID,
|
||||
); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
return previousGameID == nil || *previousGameID != gameID, tx.Commit(ctx)
|
||||
}
|
||||
|
||||
// Unbind detaches a screen session from its game.
|
||||
@@ -239,6 +306,21 @@ func (r *ScreenSessionsRepo) Unbind(ctx context.Context, id int) error {
|
||||
|
||||
// SetSound enables or disables sound for a screen session.
|
||||
func (r *ScreenSessionsRepo) SetSound(ctx context.Context, id int, enabled bool) error {
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
if _, err := tx.Exec(
|
||||
ctx,
|
||||
`SELECT id
|
||||
FROM games
|
||||
WHERE id = (SELECT game_id FROM screen_sessions WHERE id = $1)
|
||||
FOR UPDATE`,
|
||||
id,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
query := `UPDATE screen_sessions SET sound_enabled = FALSE, updated_at = NOW() WHERE id = $1`
|
||||
args := []any{id}
|
||||
if enabled {
|
||||
@@ -246,12 +328,56 @@ func (r *ScreenSessionsRepo) SetSound(ctx context.Context, id int, enabled bool)
|
||||
SET sound_enabled = (id = $1), updated_at = NOW()
|
||||
WHERE game_id = (SELECT game_id FROM screen_sessions WHERE id = $1)`
|
||||
}
|
||||
result, err := r.pool.Exec(ctx, query, args...)
|
||||
result, err := tx.Exec(ctx, query, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.RowsAffected() == 0 {
|
||||
return ErrScreenNotFound
|
||||
}
|
||||
return nil
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
// InitializeScreen copies a live display or uses the plan for the first screen.
|
||||
func (r *ScreenSessionsRepo) InitializeScreen(ctx context.Context, id, gameID int, mode int32, text, label string, duration int64) error {
|
||||
_, err := r.pool.Exec(
|
||||
ctx,
|
||||
`UPDATE screen_sessions target
|
||||
SET mode = COALESCE(source.mode, $3),
|
||||
text = COALESCE(source.text, $4),
|
||||
command = COALESCE(source.command, 0),
|
||||
position_ms = COALESCE(source.position_ms, 0),
|
||||
timer_label = COALESCE(source.timer_label, $5),
|
||||
timer_duration_seconds = COALESCE(source.timer_duration_seconds, $6),
|
||||
timer_remaining_seconds = COALESCE(source.timer_remaining_seconds, $6),
|
||||
timer_state = COALESCE(source.timer_state, 1),
|
||||
timer_ends_at = source.timer_ends_at,
|
||||
state_version = target.state_version + 1,
|
||||
updated_at = NOW()
|
||||
FROM (SELECT 1) seed
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
mode,
|
||||
text,
|
||||
command,
|
||||
position_ms,
|
||||
timer_label,
|
||||
timer_duration_seconds,
|
||||
timer_remaining_seconds,
|
||||
timer_state,
|
||||
timer_ends_at
|
||||
FROM screen_sessions
|
||||
WHERE game_id = $2 AND id <> $1
|
||||
ORDER BY updated_at DESC, id DESC
|
||||
LIMIT 1
|
||||
) source ON TRUE
|
||||
WHERE target.id = $1 AND target.game_id = $2`,
|
||||
id,
|
||||
gameID,
|
||||
mode,
|
||||
text,
|
||||
label,
|
||||
duration,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package playlist_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"evening_detective_server/internal/repos/playlists_repo"
|
||||
)
|
||||
|
||||
func TestReorderRejectsInvalidIDsBeforeRepository(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
playlistID int
|
||||
ids []int
|
||||
}{
|
||||
{"playlist", 0, []int{1}}, {"negative", 1, []int{-1}}, {"duplicate", 1, []int{1, 1}}, {"zero", 1, []int{0}},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, err := New(nil, nil).Reorder(context.Background(), tc.playlistID, tc.ids)
|
||||
if !errors.Is(err, ErrInvalidTrack) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderConflictMapping(t *testing.T) {
|
||||
for _, tc := range []struct{ input, expected error }{{playlists_repo.ErrTrackOrderChanged, ErrTrackOrderChanged}} {
|
||||
if !errors.Is(mapError(tc.input), tc.expected) {
|
||||
t.Fatal("lost conflict")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,13 +11,14 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidName = errors.New("некорректное имя плейлиста")
|
||||
ErrInvalidTrack = errors.New("некорректный трек")
|
||||
ErrPlaylistNotFound = errors.New("плейлист не найден")
|
||||
ErrPlaylistInUse = errors.New("плейлист используется")
|
||||
ErrTrackNotFound = errors.New("трек не найден")
|
||||
ErrGameNotFound = errors.New("игра не найдена")
|
||||
ErrScenarioNotFound = errors.New("сценарий не найден")
|
||||
ErrTrackOrderChanged = errors.New("состав треков изменился; обновите список")
|
||||
ErrInvalidName = errors.New("некорректное имя плейлиста")
|
||||
ErrInvalidTrack = errors.New("некорректный трек")
|
||||
ErrPlaylistNotFound = errors.New("плейлист не найден")
|
||||
ErrPlaylistInUse = errors.New("плейлист используется")
|
||||
ErrTrackNotFound = errors.New("трек не найден")
|
||||
ErrGameNotFound = errors.New("игра не найдена")
|
||||
ErrScenarioNotFound = errors.New("сценарий не найден")
|
||||
)
|
||||
|
||||
// Service implements playlist and game music use cases.
|
||||
@@ -150,6 +151,8 @@ func (s *Service) GetGame(ctx context.Context, gameID int) (*MusicState, error)
|
||||
|
||||
func mapError(err error) error {
|
||||
switch {
|
||||
case errors.Is(err, playlists_repo.ErrTrackOrderChanged):
|
||||
return ErrTrackOrderChanged
|
||||
case errors.Is(err, playlists_repo.ErrPlaylistNotFound):
|
||||
return ErrPlaylistNotFound
|
||||
case errors.Is(err, playlists_repo.ErrPlaylistInUse):
|
||||
@@ -164,3 +167,22 @@ func mapError(err error) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Reorder validates a complete track order and preserves live playback.
|
||||
func (s *Service) Reorder(ctx context.Context, playlistID int, trackIDs []int) (*Playlist, error) {
|
||||
if playlistID <= 0 {
|
||||
return nil, ErrInvalidTrack
|
||||
}
|
||||
seen := make(map[int]bool, len(trackIDs))
|
||||
for _, id := range trackIDs {
|
||||
if id <= 0 || seen[id] {
|
||||
return nil, ErrInvalidTrack
|
||||
}
|
||||
seen[id] = true
|
||||
}
|
||||
item, err := s.repo.Reorder(ctx, playlistID, trackIDs)
|
||||
if err != nil {
|
||||
return nil, mapError(err)
|
||||
}
|
||||
return mapPlaylist(item), nil
|
||||
}
|
||||
|
||||
@@ -100,15 +100,16 @@ func (s *ScreenPlanService) SetStep(ctx context.Context, gameID, currentStep int
|
||||
}
|
||||
|
||||
// ApplyCurrent applies the current step when a screen joins a game.
|
||||
func (s *ScreenPlanService) ApplyCurrent(ctx context.Context, gameID int) error {
|
||||
func (s *ScreenPlanService) ApplyCurrent(ctx context.Context, gameID, screenID int) error {
|
||||
plan, err := s.Get(ctx, gameID)
|
||||
if errors.Is(err, ErrPlanNotFound) {
|
||||
return nil
|
||||
return s.screenService.InitializeScreen(ctx, screenID, gameID, 1, "", "Перерыв", 600)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.applyStep(ctx, gameID, plan)
|
||||
step := plan.Steps[plan.CurrentStep]
|
||||
return s.screenService.InitializeScreen(ctx, screenID, gameID, step.Mode, step.Text, step.TimerLabel, step.TimerDurationSeconds)
|
||||
}
|
||||
|
||||
func normalizePlan(steps []Step, currentStep int) ([]Step, error) {
|
||||
|
||||
@@ -73,8 +73,9 @@ func (s *ScreenService) SetCommand(ctx context.Context, gameID int, command int3
|
||||
}
|
||||
|
||||
// Bind attaches a screen session to a game.
|
||||
func (s *ScreenService) Bind(ctx context.Context, id, gameID int, tokenHash string) error {
|
||||
return mapError(s.repo.Bind(ctx, id, gameID, tokenHash))
|
||||
func (s *ScreenService) Bind(ctx context.Context, id, gameID int, tokenHash string, initializeAudio bool) (bool, error) {
|
||||
isNewBinding, err := s.repo.BindWithAudio(ctx, id, gameID, tokenHash, initializeAudio)
|
||||
return isNewBinding, mapError(err)
|
||||
}
|
||||
|
||||
// Unbind detaches a screen session from its game.
|
||||
@@ -93,3 +94,8 @@ func mapError(err error) error {
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// InitializeScreen joins the current display without resetting other screens.
|
||||
func (s *ScreenService) InitializeScreen(ctx context.Context, id, gameID int, mode int32, text, label string, duration int64) error {
|
||||
return s.repo.InitializeScreen(ctx, id, gameID, mode, text, label, duration)
|
||||
}
|
||||
|
||||
+1256
-1137
File diff suppressed because it is too large
Load Diff
@@ -1084,6 +1084,51 @@ func local_request_EveningDetectiveServer_UploadPlaylistTrack_0(ctx context.Cont
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func request_EveningDetectiveServer_ReorderPlaylistTracks_0(ctx context.Context, marshaler runtime.Marshaler, client EveningDetectiveServerClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq ReorderPlaylistTracksReq
|
||||
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.ReorderPlaylistTracks(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func local_request_EveningDetectiveServer_ReorderPlaylistTracks_0(ctx context.Context, marshaler runtime.Marshaler, server EveningDetectiveServerServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq ReorderPlaylistTracksReq
|
||||
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.ReorderPlaylistTracks(ctx, &protoReq)
|
||||
return msg, metadata, err
|
||||
}
|
||||
|
||||
func request_EveningDetectiveServer_DeletePlaylistTrack_0(ctx context.Context, marshaler runtime.Marshaler, client EveningDetectiveServerClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
|
||||
var (
|
||||
protoReq DeletePlaylistTrackReq
|
||||
@@ -5177,6 +5222,26 @@ func RegisterEveningDetectiveServerHandlerServer(ctx context.Context, mux *runti
|
||||
}
|
||||
forward_EveningDetectiveServer_UploadPlaylistTrack_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodPut, pattern_EveningDetectiveServer_ReorderPlaylistTracks_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/ReorderPlaylistTracks", runtime.WithHTTPPathPattern("/api/playlists/{playlistID}/tracks/order"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := local_request_EveningDetectiveServer_ReorderPlaylistTracks_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_ReorderPlaylistTracks_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodDelete, pattern_EveningDetectiveServer_DeletePlaylistTrack_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
@@ -7415,6 +7480,23 @@ func RegisterEveningDetectiveServerHandlerClient(ctx context.Context, mux *runti
|
||||
}
|
||||
forward_EveningDetectiveServer_UploadPlaylistTrack_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodPut, pattern_EveningDetectiveServer_ReorderPlaylistTracks_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/ReorderPlaylistTracks", runtime.WithHTTPPathPattern("/api/playlists/{playlistID}/tracks/order"))
|
||||
if err != nil {
|
||||
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
|
||||
return
|
||||
}
|
||||
resp, md, err := request_EveningDetectiveServer_ReorderPlaylistTracks_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_ReorderPlaylistTracks_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
|
||||
})
|
||||
mux.Handle(http.MethodDelete, pattern_EveningDetectiveServer_DeletePlaylistTrack_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
ctx, cancel := context.WithCancel(req.Context())
|
||||
defer cancel()
|
||||
@@ -8830,6 +8912,7 @@ var (
|
||||
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_DeletePlaylist_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"api", "playlists", "ID"}, ""))
|
||||
pattern_EveningDetectiveServer_UploadPlaylistTrack_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3}, []string{"api", "playlists", "playlistID", "tracks"}, ""))
|
||||
pattern_EveningDetectiveServer_ReorderPlaylistTracks_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", "tracks", "order"}, ""))
|
||||
pattern_EveningDetectiveServer_DeletePlaylistTrack_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", "tracks", "trackID"}, ""))
|
||||
pattern_EveningDetectiveServer_SetScenarioPlaylist_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3}, []string{"api", "scenarios", "ID", "playlist"}, ""))
|
||||
pattern_EveningDetectiveServer_AddScenario_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "scenario"}, ""))
|
||||
@@ -8948,6 +9031,7 @@ var (
|
||||
forward_EveningDetectiveServer_UpdatePlaylist_0 = runtime.ForwardResponseMessage
|
||||
forward_EveningDetectiveServer_DeletePlaylist_0 = runtime.ForwardResponseMessage
|
||||
forward_EveningDetectiveServer_UploadPlaylistTrack_0 = runtime.ForwardResponseMessage
|
||||
forward_EveningDetectiveServer_ReorderPlaylistTracks_0 = runtime.ForwardResponseMessage
|
||||
forward_EveningDetectiveServer_DeletePlaylistTrack_0 = runtime.ForwardResponseMessage
|
||||
forward_EveningDetectiveServer_SetScenarioPlaylist_0 = runtime.ForwardResponseMessage
|
||||
forward_EveningDetectiveServer_AddScenario_0 = runtime.ForwardResponseMessage
|
||||
|
||||
@@ -60,6 +60,7 @@ const (
|
||||
EveningDetectiveServer_UpdatePlaylist_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/UpdatePlaylist"
|
||||
EveningDetectiveServer_DeletePlaylist_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/DeletePlaylist"
|
||||
EveningDetectiveServer_UploadPlaylistTrack_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/UploadPlaylistTrack"
|
||||
EveningDetectiveServer_ReorderPlaylistTracks_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/ReorderPlaylistTracks"
|
||||
EveningDetectiveServer_DeletePlaylistTrack_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/DeletePlaylistTrack"
|
||||
EveningDetectiveServer_SetScenarioPlaylist_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/SetScenarioPlaylist"
|
||||
EveningDetectiveServer_AddScenario_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/AddScenario"
|
||||
@@ -183,6 +184,7 @@ type EveningDetectiveServerClient interface {
|
||||
UpdatePlaylist(ctx context.Context, in *UpdatePlaylistReq, opts ...grpc.CallOption) (*UpdatePlaylistRsp, error)
|
||||
DeletePlaylist(ctx context.Context, in *DeletePlaylistReq, opts ...grpc.CallOption) (*DeletePlaylistRsp, error)
|
||||
UploadPlaylistTrack(ctx context.Context, in *UploadPlaylistTrackReq, opts ...grpc.CallOption) (*UploadPlaylistTrackRsp, error)
|
||||
ReorderPlaylistTracks(ctx context.Context, in *ReorderPlaylistTracksReq, opts ...grpc.CallOption) (*ReorderPlaylistTracksRsp, error)
|
||||
DeletePlaylistTrack(ctx context.Context, in *DeletePlaylistTrackReq, opts ...grpc.CallOption) (*DeletePlaylistTrackRsp, error)
|
||||
SetScenarioPlaylist(ctx context.Context, in *SetScenarioPlaylistReq, opts ...grpc.CallOption) (*SetScenarioPlaylistRsp, error)
|
||||
AddScenario(ctx context.Context, in *AddScenarioReq, opts ...grpc.CallOption) (*AddScenarioRsp, error)
|
||||
@@ -626,6 +628,16 @@ func (c *eveningDetectiveServerClient) UploadPlaylistTrack(ctx context.Context,
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *eveningDetectiveServerClient) ReorderPlaylistTracks(ctx context.Context, in *ReorderPlaylistTracksReq, opts ...grpc.CallOption) (*ReorderPlaylistTracksRsp, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ReorderPlaylistTracksRsp)
|
||||
err := c.cc.Invoke(ctx, EveningDetectiveServer_ReorderPlaylistTracks_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *eveningDetectiveServerClient) DeletePlaylistTrack(ctx context.Context, in *DeletePlaylistTrackReq, opts ...grpc.CallOption) (*DeletePlaylistTrackRsp, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(DeletePlaylistTrackRsp)
|
||||
@@ -1476,6 +1488,7 @@ type EveningDetectiveServerServer interface {
|
||||
UpdatePlaylist(context.Context, *UpdatePlaylistReq) (*UpdatePlaylistRsp, error)
|
||||
DeletePlaylist(context.Context, *DeletePlaylistReq) (*DeletePlaylistRsp, error)
|
||||
UploadPlaylistTrack(context.Context, *UploadPlaylistTrackReq) (*UploadPlaylistTrackRsp, error)
|
||||
ReorderPlaylistTracks(context.Context, *ReorderPlaylistTracksReq) (*ReorderPlaylistTracksRsp, error)
|
||||
DeletePlaylistTrack(context.Context, *DeletePlaylistTrackReq) (*DeletePlaylistTrackRsp, error)
|
||||
SetScenarioPlaylist(context.Context, *SetScenarioPlaylistReq) (*SetScenarioPlaylistRsp, error)
|
||||
AddScenario(context.Context, *AddScenarioReq) (*AddScenarioRsp, error)
|
||||
@@ -1681,6 +1694,9 @@ func (UnimplementedEveningDetectiveServerServer) DeletePlaylist(context.Context,
|
||||
func (UnimplementedEveningDetectiveServerServer) UploadPlaylistTrack(context.Context, *UploadPlaylistTrackReq) (*UploadPlaylistTrackRsp, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method UploadPlaylistTrack not implemented")
|
||||
}
|
||||
func (UnimplementedEveningDetectiveServerServer) ReorderPlaylistTracks(context.Context, *ReorderPlaylistTracksReq) (*ReorderPlaylistTracksRsp, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ReorderPlaylistTracks not implemented")
|
||||
}
|
||||
func (UnimplementedEveningDetectiveServerServer) DeletePlaylistTrack(context.Context, *DeletePlaylistTrackReq) (*DeletePlaylistTrackRsp, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method DeletePlaylistTrack not implemented")
|
||||
}
|
||||
@@ -2558,6 +2574,24 @@ func _EveningDetectiveServer_UploadPlaylistTrack_Handler(srv interface{}, ctx co
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _EveningDetectiveServer_ReorderPlaylistTracks_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ReorderPlaylistTracksReq)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(EveningDetectiveServerServer).ReorderPlaylistTracks(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: EveningDetectiveServer_ReorderPlaylistTracks_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(EveningDetectiveServerServer).ReorderPlaylistTracks(ctx, req.(*ReorderPlaylistTracksReq))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _EveningDetectiveServer_DeletePlaylistTrack_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(DeletePlaylistTrackReq)
|
||||
if err := dec(in); err != nil {
|
||||
@@ -4159,6 +4193,10 @@ var EveningDetectiveServer_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "UploadPlaylistTrack",
|
||||
Handler: _EveningDetectiveServer_UploadPlaylistTrack_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ReorderPlaylistTracks",
|
||||
Handler: _EveningDetectiveServer_ReorderPlaylistTracks_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "DeletePlaylistTrack",
|
||||
Handler: _EveningDetectiveServer_DeletePlaylistTrack_Handler,
|
||||
|
||||
Reference in New Issue
Block a user