Skip to content

Commit 228025f

Browse files
committed
deep link
1 parent 3d7fdee commit 228025f

File tree

4 files changed

+142
-67
lines changed

4 files changed

+142
-67
lines changed

lib/pling/playlists/music_library.ex

Lines changed: 82 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ defmodule Pling.Playlists.MusicLibrary do
44
Playlists are organized by decades, with tracks associated to each playlist.
55
"""
66
import Ecto.Query
7+
require Logger
78
alias Pling.Repo
89
alias Pling.Playlists.{Playlist, Track}
910

@@ -56,46 +57,104 @@ defmodule Pling.Playlists.MusicLibrary do
5657

5758
@doc """
5859
Gets a playlist by Spotify ID, fetching it from Spotify if it doesn't exist locally.
60+
Returns the playlist as soon as it has basic info and at least one track.
5961
"""
60-
def get_or_fetch_playlist(spotify_id) do
62+
def get_or_fetch_playlist(spotify_id, timeout \\ 5000) do
6163
case Repo.get(Playlist, spotify_id) do
6264
nil ->
63-
# Fetch from Spotify and save
64-
case Pling.Services.Spotify.get_playlist(spotify_id) do
65-
{:ok, %{playlist_info: info, tracks: tracks}} ->
66-
# Create and save the playlist
65+
# Create a reference we can use to receive a message when the first track arrives
66+
ref = make_ref()
67+
parent = self()
68+
69+
callback = fn
70+
%{"playlist_info" => info} ->
6771
playlist = %Playlist{
6872
spotify_id: spotify_id,
69-
name: info.name,
70-
owner: info.owner,
71-
image_url: info.image_url,
73+
name: info["name"],
74+
owner: info["owner"],
75+
image_url: get_first_image_url(info["images"]),
7276
official: false
7377
}
7478

7579
{:ok, saved_playlist} = Repo.insert(playlist)
80+
saved_playlist
7681

77-
# Save all tracks
78-
Enum.each(tracks, fn track_data ->
79-
%Track{
80-
spotify_id: track_data.spotify_id,
81-
title: track_data.title,
82-
artists: track_data.artists,
83-
uri: track_data.uri,
84-
popularity: track_data.popularity,
85-
album: track_data.album,
86-
playlist_spotify_id: spotify_id
87-
}
88-
|> Repo.insert(on_conflict: :nothing)
89-
end)
82+
%{"track" => track} ->
83+
track_entry = %Track{
84+
spotify_id: track["id"],
85+
title: track["name"],
86+
artists: Enum.map(track["artists"], & &1["name"]),
87+
uri: track["uri"],
88+
popularity: track["popularity"],
89+
album: track["album"]["name"],
90+
playlist_spotify_id: spotify_id
91+
}
9092

91-
saved_playlist
93+
{:ok, saved_track} = Repo.insert(track_entry, on_conflict: :nothing)
94+
95+
# Notify parent process of first track
96+
if not Process.get({:notified_first_track, ref}) do
97+
Process.put({:notified_first_track, ref}, true)
98+
send(parent, {:first_track_saved, ref})
99+
end
100+
101+
saved_track
102+
end
103+
104+
# Start streaming in the background
105+
Task.start(fn ->
106+
case Pling.Services.Spotify.stream_playlist(spotify_id, callback) do
107+
:ok ->
108+
:ok
109+
110+
{:error, reason} ->
111+
Logger.error("Failed to fetch complete playlist: #{inspect(reason)}")
112+
# Notify parent in case of immediate error
113+
send(parent, {:playlist_error, ref, reason})
114+
end
115+
end)
92116

93-
{:error, reason} ->
117+
# Wait for either first track or timeout
118+
receive do
119+
{:first_track_saved, ^ref} ->
120+
case Repo.get(Playlist, spotify_id) do
121+
nil -> {:error, :playlist_fetch_failed}
122+
playlist -> playlist
123+
end
124+
125+
{:playlist_error, ^ref, reason} ->
94126
{:error, reason}
127+
after
128+
timeout ->
129+
case Repo.get(Playlist, spotify_id) do
130+
nil -> {:error, :playlist_fetch_timeout}
131+
playlist -> playlist
132+
end
95133
end
96134

97135
playlist ->
98136
playlist
99137
end
100138
end
139+
140+
@doc """
141+
Returns a callback that just outputs the streaming data.
142+
Useful for debugging or monitoring the stream.
143+
"""
144+
def debug_callback do
145+
fn
146+
%{"playlist_info" => info} ->
147+
IO.puts("Playlist Info:")
148+
IO.inspect(info, pretty: true)
149+
:ok
150+
151+
%{"track" => track} ->
152+
IO.puts("\nTrack:")
153+
IO.inspect(track, pretty: true)
154+
:ok
155+
end
156+
end
157+
158+
defp get_first_image_url([%{"url" => url} | _]), do: url
159+
defp get_first_image_url(_), do: nil
101160
end

lib/pling/rooms/room/impl.ex

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,19 @@ defmodule Pling.Rooms.Room.Impl do
88

99
def initialize(room_code, game_mode, leader_id, playlist \\ nil) do
1010
initial_state = RoomState.initialize(room_code, game_mode, leader_id)
11+
playlists = MusicLibrary.load_playlists()
1112

1213
state =
1314
if playlist do
15+
# Merge the custom playlist with existing playlists
16+
updated_playlists = Map.put(playlists, playlist.spotify_id, playlist)
17+
1418
%{
1519
initial_state
16-
| playlists: %{"custom" => playlist},
17-
selection: %{playlist: "custom", track: nil}
20+
| playlists: updated_playlists,
21+
selection: %{playlist: playlist.spotify_id, track: nil}
1822
}
1923
else
20-
playlists = MusicLibrary.load_playlists()
21-
2224
if Enum.empty?(playlists) do
2325
Logger.warning("No playlists found in database, room may not function correctly")
2426

lib/pling/services/spotify.ex

Lines changed: 35 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ defmodule Pling.Services.Spotify do
44
@base_url "https://playlist-fetcher-1031294514094.europe-north1.run.app"
55
@api_key "1367a6ef2cb6f3cd2bfb3f73978292fdae3bc9a5e885ca7632d4eb3fc15eb4d4"
66

7-
def get_playlist(playlist_id) do
7+
def stream_playlist(playlist_id, callback) when is_function(callback, 1) do
8+
Logger.info("Starting playlist stream", playlist_id: playlist_id)
9+
810
req =
911
Req.new(
1012
base_url: @base_url,
@@ -13,44 +15,42 @@ defmodule Pling.Services.Spotify do
1315

1416
case Req.get(req,
1517
url: "/playlist/#{playlist_id}",
16-
params: [stream: false]
18+
params: [stream: true],
19+
into: fn {:data, data}, {req, resp} ->
20+
String.split(data, "\n", trim: true)
21+
|> Enum.each(fn line ->
22+
case Jason.decode(line) do
23+
{:ok, decoded} ->
24+
Logger.debug("Processing playlist item",
25+
playlist_id: playlist_id,
26+
track_id: decoded["id"]
27+
)
28+
29+
callback.(decoded)
30+
31+
{:error, error} ->
32+
Logger.error("Failed to decode chunk",
33+
playlist_id: playlist_id,
34+
error: inspect(error),
35+
chunk: line
36+
)
37+
end
38+
end)
39+
40+
{:cont, {req, resp}}
41+
end
1742
) do
18-
{:ok, %{body: %{"tracks" => tracks, "playlist_info" => playlist_info}}} ->
19-
{:ok,
20-
%{
21-
tracks: Enum.map(tracks, &map_to_track/1),
22-
playlist_info: map_to_playlist_info(playlist_info)
23-
}}
43+
{:ok, _response} ->
44+
Logger.info("Completed playlist stream", playlist_id: playlist_id)
45+
:ok
2446

2547
{:error, error} ->
26-
{:error, error}
48+
Logger.error("Failed to stream playlist",
49+
playlist_id: playlist_id,
50+
error: inspect(error)
51+
)
2752

28-
unexpected ->
29-
Logger.error("Unexpected response from Spotify service: #{inspect(unexpected)}")
30-
{:error, :invalid_response}
53+
{:error, error}
3154
end
3255
end
33-
34-
defp map_to_track(%{"track" => track}) do
35-
%Pling.Playlists.Track{
36-
spotify_id: track["id"],
37-
title: track["name"],
38-
artists: Enum.map(track["artists"], & &1["name"]),
39-
uri: track["uri"],
40-
popularity: track["popularity"],
41-
album: track["album"]["name"]
42-
}
43-
end
44-
45-
defp map_to_playlist_info(playlist_info) do
46-
%{
47-
name: playlist_info["name"],
48-
owner: playlist_info["owner"],
49-
uri: playlist_info["uri"],
50-
image_url: get_first_image_url(playlist_info["images"])
51-
}
52-
end
53-
54-
defp get_first_image_url([%{"url" => url} | _]), do: url
55-
defp get_first_image_url(_), do: nil
5656
end

lib/pling_web/live/room_live.ex

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,15 @@ defmodule PlingWeb.RoomLive do
1818
case params do
1919
%{"playlist_id" => playlist_id} ->
2020
# Ensure playlist exists or fetch it
21-
playlist = Pling.Playlists.MusicLibrary.get_or_fetch_playlist(playlist_id)
22-
Rooms.join_room(room_code, user_id, self(), game_mode, playlist)
21+
case Pling.Playlists.MusicLibrary.get_or_fetch_playlist(playlist_id) do
22+
{:error, reason} ->
23+
# If playlist fetch fails, fall back to default behavior
24+
socket = put_flash(socket, :error, "Failed to load playlist: #{reason}")
25+
Rooms.join_room(room_code, user_id, self(), game_mode)
26+
27+
playlist when is_struct(playlist) ->
28+
Rooms.join_room(room_code, user_id, self(), game_mode, playlist)
29+
end
2330

2431
_ ->
2532
Rooms.join_room(room_code, user_id, self(), game_mode)
@@ -36,8 +43,14 @@ defmodule PlingWeb.RoomLive do
3643
{:ok, state} =
3744
case params do
3845
%{"playlist_id" => playlist_id} ->
39-
playlist = Pling.Playlists.MusicLibrary.get_or_fetch_playlist(playlist_id)
40-
Rooms.join_room(room_code, user_id, self(), game_mode, playlist)
46+
case Pling.Playlists.MusicLibrary.get_or_fetch_playlist(playlist_id) do
47+
{:error, reason} ->
48+
socket = put_flash(socket, :error, "Failed to load playlist: #{reason}")
49+
Rooms.join_room(room_code, user_id, self(), game_mode)
50+
51+
playlist when is_struct(playlist) ->
52+
Rooms.join_room(room_code, user_id, self(), game_mode, playlist)
53+
end
4154

4255
_ ->
4356
Rooms.join_room(room_code, user_id, self(), game_mode)
@@ -64,6 +77,7 @@ defmodule PlingWeb.RoomLive do
6477
leader?: false,
6578
users: [],
6679
selection: %{playlist: nil, track: nil},
80+
# This will be populated from room state
6781
playlists: nil
6882
)
6983
end
@@ -225,7 +239,7 @@ defmodule PlingWeb.RoomLive do
225239
end
226240

227241
def pling_button(assigns) do
228-
assigns = assign(assigns, :disabled?, !assigns.leader? && !assigns.playing?)
242+
assigns = assign(assigns, :disabled?, !assigns.playing? && !assigns.leader?)
229243

230244
~H"""
231245
<div id="start" phx-hook="PlingButton" class="flex place-content-center w-full">

0 commit comments

Comments
 (0)