Skip to content

Commit 3d37717

Browse files
committed
implement playlist queue for room server
1 parent 1482896 commit 3d37717

File tree

3 files changed

+171
-9
lines changed

3 files changed

+171
-9
lines changed

lib/pling/rooms/room/impl.ex

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,10 @@ defmodule Pling.Rooms.Room.Impl do
1818
%{
1919
initial_state
2020
| playlists: updated_playlists,
21-
selection: %{playlist: playlist.spotify_id, track: track}
21+
selection: %{playlist: playlist.spotify_id, track: track, queue: []}
2222
}
2323
else
24-
initial_state = %{initial_state | playlists: playlists}
25-
update_track(initial_state)
24+
%{initial_state | playlists: playlists}
2625
end
2726

2827
update_track(state)
@@ -42,20 +41,36 @@ defmodule Pling.Rooms.Room.Impl do
4241
end
4342

4443
def update_track(state) do
45-
track = select_track(state)
46-
%{state | selection: %{playlist: state.selection.playlist, track: track}}
44+
{track, queue} = select_track(state)
45+
%{state |
46+
selection: %{
47+
playlist: state.selection.playlist,
48+
track: track,
49+
queue: [track | queue]
50+
}
51+
}
4752
end
4853

49-
defp select_track(%{playlists: playlists} = state) do
50-
MusicLibrary.select_track(playlists, state.selection.playlist)
54+
defp select_track(%{playlists: playlists, selection: %{queue: []}} = state) do
55+
tracks = MusicLibrary.get_tracks(state.selection.playlist, playlists)
56+
shuffled = Enum.shuffle(tracks)
57+
{hd(shuffled), tl(shuffled)}
58+
end
59+
60+
defp select_track(%{selection: %{queue: [_current | remaining]}} = _state) do
61+
{hd(remaining), tl(remaining)}
5162
end
5263

5364
def set_playlist(state, playlist_id) do
5465
playlists = MusicLibrary.load_playlists()
5566

5667
state
5768
|> Map.put(:playlists, playlists)
58-
|> Map.put(:selection, %{playlist: playlist_id, track: nil})
69+
|> Map.put(:selection, %{
70+
playlist: playlist_id,
71+
track: nil,
72+
queue: []
73+
})
5974
|> update_track()
6075
|> reset_playback()
6176
end

lib/pling/rooms/room_state.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ defmodule Pling.Rooms.RoomState do
2525
recent_plings: [],
2626
selection: %{
2727
playlist: @default_playlist_id,
28-
track: nil
28+
track: nil,
29+
queue: [] # First item is always the current track
2930
}
3031
}
3132
end
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
defmodule Pling.Rooms.Room.ImplTest do
2+
use Pling.DataCase
3+
alias Pling.Rooms.Room.Impl
4+
alias Pling.Playlists.{Playlist, Track}
5+
6+
@default_playlist_id "6ZSeHvrhmEH4erjxudpULB"
7+
8+
setup do
9+
:meck.unload()
10+
:meck.new(Pling.Playlists.MusicLibrary, [:passthrough])
11+
12+
tracks = [
13+
%Track{uri: "track1", title: "Track 1"},
14+
%Track{uri: "track2", title: "Track 2"},
15+
%Track{uri: "track3", title: "Track 3"},
16+
%Track{uri: "track4", title: "Track 4"}
17+
]
18+
19+
playlist = %Playlist{
20+
spotify_id: "test_playlist",
21+
name: "Test Playlist",
22+
tracks: tracks
23+
}
24+
25+
default_playlist = %Playlist{
26+
spotify_id: @default_playlist_id,
27+
name: "Default Playlist",
28+
tracks: tracks
29+
}
30+
31+
playlists = %{
32+
playlist.spotify_id => playlist,
33+
@default_playlist_id => default_playlist
34+
}
35+
36+
:meck.expect(Pling.Playlists.MusicLibrary, :load_playlists, fn -> playlists end)
37+
:meck.expect(Pling.Playlists.MusicLibrary, :get_tracks, fn playlist_id, _playlists ->
38+
case playlist_id do
39+
@default_playlist_id -> tracks
40+
"test_playlist" -> tracks
41+
_ -> []
42+
end
43+
end)
44+
45+
on_exit(fn -> :meck.unload() end)
46+
47+
{:ok, %{playlist: playlist, tracks: tracks}}
48+
end
49+
50+
describe "initialize/4" do
51+
test "initializes room with default playlist", %{tracks: tracks} do
52+
state = Impl.initialize("room1", "vs", "leader1")
53+
54+
assert state.room_code == "room1"
55+
assert state.game_mode == "vs"
56+
assert state.leader_id == "leader1"
57+
assert state.playing? == false
58+
assert Map.has_key?(state.playlists, @default_playlist_id)
59+
assert state.selection.track in tracks
60+
assert state.selection.queue != nil
61+
assert length(state.selection.queue) > 0
62+
assert length(state.selection.queue) == length(tracks)
63+
end
64+
65+
test "initializes room with provided playlist", %{playlist: playlist, tracks: tracks} do
66+
state = Impl.initialize("room1", "vs", "leader1", playlist)
67+
68+
assert state.selection.playlist == playlist.spotify_id
69+
assert state.selection.track in tracks
70+
assert is_list(state.selection.queue)
71+
assert length(state.selection.queue) == length(tracks)
72+
assert state.selection.queue != tracks
73+
end
74+
end
75+
76+
describe "play/1 and pause/1" do
77+
test "transitions between playing states", %{tracks: tracks} do
78+
initial_state = Impl.initialize("room1", "vs", "leader1")
79+
80+
assert initial_state.selection.track in tracks
81+
82+
played_state = Impl.play(initial_state)
83+
assert played_state.playing? == true
84+
assert played_state.countdown != nil
85+
assert played_state.timer_ref != nil
86+
87+
paused_state = Impl.pause(played_state)
88+
assert paused_state.playing? == false
89+
assert paused_state.countdown == nil
90+
assert paused_state.timer_ref == nil
91+
end
92+
end
93+
94+
describe "update_track/1" do
95+
test "updates track and maintains queue order", %{tracks: tracks} do
96+
state = Impl.initialize("room1", "vs", "leader1")
97+
initial_track = state.selection.track
98+
99+
# First update - should take next track from queue
100+
updated_state = Impl.update_track(state)
101+
assert updated_state.selection.track != initial_track
102+
assert updated_state.selection.track in tracks
103+
assert hd(updated_state.selection.queue) == updated_state.selection.track
104+
105+
# Second update - should take next track
106+
second_update = Impl.update_track(updated_state)
107+
assert second_update.selection.track != updated_state.selection.track
108+
assert second_update.selection.track in tracks
109+
assert hd(second_update.selection.queue) == second_update.selection.track
110+
end
111+
112+
test "reshuffles with all tracks when queue is empty", %{tracks: tracks} do
113+
state = Impl.initialize("room1", "vs", "leader1")
114+
115+
# Empty the queue
116+
empty_state = %{state | selection: %{state.selection | queue: []}}
117+
118+
# Update should trigger reshuffle
119+
new_state = Impl.update_track(empty_state)
120+
assert new_state.selection.track in tracks
121+
assert length(new_state.selection.queue) == length(tracks)
122+
assert hd(new_state.selection.queue) == new_state.selection.track
123+
assert Enum.sort(new_state.selection.queue) == Enum.sort(tracks)
124+
end
125+
126+
test "maintains unique tracks in queue until reshuffle", %{tracks: tracks} do
127+
state = Impl.initialize("room1", "vs", "leader1")
128+
129+
# Track all updates until we've seen all tracks
130+
{_final_state, seen_tracks} =
131+
Enum.reduce_while(1..10, {state, MapSet.new([state.selection.track])}, fn _, {current_state, seen} ->
132+
new_state = Impl.update_track(current_state)
133+
new_seen = MapSet.put(seen, new_state.selection.track)
134+
135+
if MapSet.size(new_seen) == length(tracks) do
136+
{:halt, {new_state, new_seen}}
137+
else
138+
{:cont, {new_state, new_seen}}
139+
end
140+
end)
141+
142+
assert MapSet.size(seen_tracks) == length(tracks)
143+
assert MapSet.to_list(seen_tracks) |> Enum.sort() == Enum.sort(tracks)
144+
end
145+
end
146+
end

0 commit comments

Comments
 (0)