This commit is contained in:
2026-10-27 15:16:37 +07:00
parent e4c8116e36
commit e68cf4322c
13 changed files with 162 additions and 16 deletions
@@ -36,7 +36,7 @@ func TestExportSnapshotAndEditableGame(t *testing.T) {
requireNoError(t, err)
selected, err := music.Create(ctx, "Selected by screen")
requireNoError(t, err)
requireNoError(t, music.AddTrack(ctx, selected.ID, "Grouped", "grouped.mp3"))
requireNoError(t, music.AddTrack(ctx, selected.ID, 0, "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)
+3 -3
View File
@@ -16,7 +16,7 @@ func TestGroupEditingProtectsLivePlayback(t *testing.T) {
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"))
requireNoError(t, f.repo.AddTrack(ctx, p.ID, 0, name, name+".mp3"))
}
p, err = f.repo.SaveGroup(ctx, p.ID, 0, "First", "repeat")
requireNoError(t, err)
@@ -101,7 +101,7 @@ func TestGroupAssignmentsAndPlaybackRejectForeignIDs(t *testing.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"))
requireNoError(t, f.repo.AddTrack(ctx, p.ID, 0, "Track", "track.mp3"))
p, err = f.repo.SaveGroup(ctx, p.ID, 0, "Empty", "repeat")
requireNoError(t, err)
other, err := f.repo.Create(ctx, "Other")
@@ -132,7 +132,7 @@ func TestConcurrentCompletionAdvancesOnce(t *testing.T) {
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"))
requireNoError(t, f.repo.AddTrack(ctx, p.ID, 0, name, name+".mp3"))
}
before, err := f.repo.SetMusic(ctx, f.gameID, p.ID, 0, 0, 50, false, playback, 0)
requireNoError(t, err)
+3 -3
View File
@@ -20,7 +20,7 @@ func TestReorderPreservesPlaybackAndRejectsChangedMembership(t *testing.T) {
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"))
requireNoError(t, f.repo.AddTrack(ctx, p.ID, 0, name, name+".mp3"))
}
p, err = f.repo.Get(ctx, p.ID)
requireNoError(t, err)
@@ -90,7 +90,7 @@ func TestConcurrentAdditionsKeepDistinctPositions(t *testing.T) {
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.Go(func() { failures <- f.repo.AddTrack(ctx, p.ID, 0, "Track", fmt.Sprintf("track-%d.mp3", i)) })
}
wg.Wait()
close(failures)
@@ -172,7 +172,7 @@ func TestReorderReadersSeeConsistentTrackAndIndex(t *testing.T) {
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"))
requireNoError(t, f.repo.AddTrack(ctx, p.ID, 0, name, name+".mp3"))
}
p, err = f.repo.Get(ctx, p.ID)
requireNoError(t, err)
+12 -3
View File
@@ -242,7 +242,7 @@ func (r *Repo) Delete(ctx context.Context, id int) error {
}
// AddTrack adds a track to a playlist.
func (r *Repo) AddTrack(ctx context.Context, playlistID int, name, filename string) error {
func (r *Repo) AddTrack(ctx context.Context, playlistID, groupID int, name, filename string) error {
tx, err := r.pool.Begin(ctx)
if err != nil {
return err
@@ -251,6 +251,14 @@ func (r *Repo) AddTrack(ctx context.Context, playlistID int, name, filename stri
if err := lockPlaylist(ctx, tx, playlistID); err != nil {
return err
}
if groupID != 0 {
if _, err := readGroup(ctx, tx, playlistID, groupID); err != nil {
return err
}
if err := groupInUse(ctx, tx, playlistID, groupID); err != nil {
return err
}
}
var position int
if err := tx.QueryRow(
ctx,
@@ -266,13 +274,14 @@ func (r *Repo) AddTrack(ctx context.Context, playlistID int, name, filename stri
_, err = tx.Exec(
ctx,
`
INSERT INTO playlist_tracks (playlist_id, name, filename, position)
VALUES ($1, $2, $3, $4)
INSERT INTO playlist_tracks (playlist_id, name, filename, position, group_id)
VALUES ($1, $2, $3, $4, NULLIF($5, 0))
`,
playlistID,
name,
filename,
position,
groupID,
)
if err != nil {
return err
@@ -0,0 +1,93 @@
package playlists_repo
import (
"context"
"errors"
"reflect"
"testing"
)
func TestAddTrackSavesTargetGroupAtomically(t *testing.T) {
cases := []struct {
name string
target string
playback int32
activeGroup bool
want error
}{
{name: "ungrouped", target: "none"},
{name: "empty group", target: "second"},
{name: "populated group", target: "first"},
{name: "missing group", target: "missing", want: ErrGroupNotFound},
{name: "foreign group", target: "foreign", want: ErrGroupNotFound},
{name: "deleted group", target: "deleted", want: ErrGroupNotFound},
{name: "playing group", target: "first", playback: 2, activeGroup: true, want: ErrPlaylistInUse},
{name: "paused group", target: "first", playback: 3, activeGroup: true, want: ErrPlaylistInUse},
{name: "another group playing", target: "second", playback: 2, activeGroup: true},
{name: "whole playlist playing", target: "second", playback: 2, want: ErrPlaylistInUse},
{name: "legacy ungrouped upload during playback", target: "none", playback: 2},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
f := newPlaylistFixture(t)
ctx := context.Background()
p, err := f.repo.Create(ctx, "Upload")
requireNoError(t, err)
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
requireNoError(t, f.repo.AddTrack(ctx, p.ID, first, "Existing", "existing.mp3"))
groupID := first
switch tc.target {
case "none":
groupID = 0
case "second", "deleted":
groupID = second
case "missing":
groupID = second + 1000000
case "foreign":
other, err := f.repo.Create(ctx, "Other")
requireNoError(t, err)
other, err = f.repo.SaveGroup(ctx, other.ID, 0, "Foreign", "repeat")
requireNoError(t, err)
groupID = other.Groups[0].ID
}
if tc.target == "deleted" {
_, err := f.repo.DeleteGroup(ctx, p.ID, groupID)
requireNoError(t, err)
}
if tc.playback != 0 {
activeGroup := 0
if tc.activeGroup {
activeGroup = first
}
_, err := f.repo.SetMusic(ctx, f.gameID, p.ID, 0, 0, 50, false, tc.playback, activeGroup)
requireNoError(t, err)
}
before, err := f.repo.Get(ctx, p.ID)
requireNoError(t, err)
err = f.repo.AddTrack(ctx, p.ID, groupID, "New", "new.mp3")
if !errors.Is(err, tc.want) {
t.Fatalf("expected %v, got %v", tc.want, err)
}
after, err := f.repo.Get(ctx, p.ID)
requireNoError(t, err)
if tc.want != nil {
if !reflect.DeepEqual(before.Tracks, after.Tracks) {
t.Fatal("rejected upload changed playlist tracks")
}
return
}
if len(after.Tracks) != len(before.Tracks)+1 {
t.Fatal("upload did not add exactly one track")
}
track := after.Tracks[len(after.Tracks)-1]
if track.GroupID != groupID || track.Name != "New" || track.Position != before.Tracks[len(before.Tracks)-1].Position+1 {
t.Fatalf("track was not appended to the target group: %+v", track)
}
})
}
}