Showcase: Spotify to Roblox display

Not sure what I should’ve listed this post as but I’m pretty sure this works?

SHOWCASE VIDEO AT THE BOTTOM … or here
.
.
I had recently started to get into API’s in Roblox which had sparked a great idea for a game, a social hangout where people can see what others are listening to on Spotify, this would have been very interactive and cool to see on Roblox.


API Limitations & Why This Isn’t Fully Public

Having seen the Spotify API, I knew my game could be done.
And I had done it, within one day, I had made the game. But with a major flaw, reading the new Spotify API TOS if I wanted to go mainstream I would need a certain upgrade to my API, only issue. I need to fill a certain criteria.


Easy to see, as a single Roblox developer I have no chance to be able to use the full API, which had only recently been updated to have criteria needs.
image
But alas with my developer API I was able to host 25 players if I had their Email within the API database, so for fun I was able to play around with it with some of my friends.

Backend: Bridging Roblox & Spotify | How It Works ( Off Platform )

Since Roblox doesn’t support direct Spotify integration, I built an Express.js (Node.js) server to act as a middleman between Spotify’s Web API and Roblox’s HttpService.

The backend handler works with Node.js
It handles:

  • Handle OAuth login/token storage
  • Query Spotify’s /me/player endpoint
  • Return playback data (track, artist, progress) in JSON
  • Hosting the endpoint via Render.com ( Seemed to have free hosting for small projects )

server.js (Express server):

// Example endpoint
app.get("/spotify/status/:userid", async (req, res) => {
  const userId = req.params.userid;
  const token = getTokenForUser(userId); // Stored access token

  const response = await axios.get("https://api.spotify.com/v1/me/player", {
    headers: { Authorization: `Bearer ${token}` }
  });

  const body = response.data;

  res.json({
    track: body.item?.name || null,
    artist: (body.item?.artists || []).map(a => a.name).join(', ') || null,
    is_playing: body.is_playing || false,
    album: body?.item?.album?.name,
    duration_ms: body?.item?.duration_ms,
    progress_ms: body?.progress_ms
  });
});

How It Works:

  1. Roblox calls https://yourserver.com/spotify/status/"USERID"
  2. Server looks up USERID’s access token
  3. Makes request to Spotify’s /me/player endpoint
  4. Sends JSON with track, artist, progress, etc.

Hosting:

  • Code pushed to GitHub
  • Deployed on Render.com for live hosting ( For a Temporary Time )
  • If the project ever scaled, I planned to host it on my own or a friend’s domain

Roblox Integration

Responsibilities:

  • Listen for RemoteEvents
  • Request data from the backend
  • Attach a floating display UI to the player’s Head
  • Continuously update the UI as songs progress

ServerScript part

players.PlayerAdded:Connect(function(p)
	p.CharacterAdded:Wait()
	module.start(p, 'cre') -- Create on spawn
end)

sendEvent.OnServerEvent:Connect(function(p)
	module.start(p, 'cre') -- Manual create
end)

updateEvent.OnServerEvent:Connect(function(p)
	p.Character.Head:WaitForChild("SpotifyDisplay"):Destroy()
	module.start(p, 'cre') -- Refresh
end)

3 ways to get the display

  • Joining ( Account Linked Prior )
  • start button being pressed after linking
  • update button being pressed, given to players after confirmed linkage (Just a refresh button)

Automating It

To avoid flooding the backend with requests, I implemented a timer-based updater in a ModuleScript. It animates the song progress locally, then fetches new data when the song ends.

Example:

local function createTimer(player, label: TextLabel, currentMS, durMS)
	task.spawn(function()
		local cur = math.floor(currentMS / 1000)
		local dur = math.floor(durMS / 1000)

		for i = cur, dur - 1 do
			if label == nil or not label:IsDescendantOf(game) then break end
			label.Text = "Progress: " .. formatTime(i * 1000) .. " / " .. formatTime(dur * 1000)
			wait(1)
		end

		print("END — scheduling next update")
		getSpotifyStatus(player, "upd")
	end)
end

Once the timer had ended It would call the main function

function getSpotifyStatus(player: Player, request)
	local userId = tostring(player.UserId)
	local url = "https://spotify-to-roblox.onrender.com/spotify/status/" .. userId -- The hosted Roblox to Spotify Linker

	local success, result = pcall(function()
		return HttpService:GetAsync(url)
	end)

	if not success then
		warn("Failed to fetch Spotify status:", result)
		player.PlayerGui.MainGui.linkProfile.Visible = true
		return
	end
	-- print("Raw response:", result)

	local data
	local decodeSuccess, decodeError = pcall(function()
		data = HttpService:JSONDecode(result)
	end)

	if not decodeSuccess then
		warn("JSON decoding failed:", decodeError)
		return
	end

	if data.error then
		warn("Spotify API error:", data.error)
	elseif typeof(data.track) == "string" and typeof(data.artist) == "string" then
		if request == "cre" then
			createDisplay(player, data)
		elseif request == "upd" then
			local head = player.Character and player.Character:FindFirstChild("Head")
			if head then
				local gui = head:FindFirstChild("SpotifyDisplay")
				if gui then
					updateDisplay(player, gui:FindFirstChild("Frame"), data)
				else
					warn("No display to update")
				end
			end
		else
			warn(request .. " is not a valid Request")
		end
		player.PlayerGui.MainGui.update.Visible = true
	else
		print("Invalid or empty data received. Track:", data.track, "Artist:", data.artist)
	end
end
View Full Dissection

Purpose:

This function fetches a player’s currently playing Spotify track using their stored user ID and sends the response to either create or update the UI in Roblox.


Dissected Function:

function getSpotifyStatus(player: Player, request)
	local url = "https://spotify-to-roblox.onrender.com/spotify/status/" .. player.UserId
  • request: A string that decides what to do with the data ("cre" = create UI, "upd" = update UI).
  • url: The custom endpoint you made that returns the player’s Spotify playback data.

	local success, result = pcall(function()
		return HttpService:GetAsync(url)
	end)
  • pcall: Ensures the request won’t crash the script if it fails. It safely captures errors.
  • HttpService:GetAsync(url): Makes an HTTP GET request to the Express server.
  • success: Boolean result of whether the request worked.
  • result: The raw JSON string from the server or an error message.

	if not success then
		warn("Fetch failed:", result)
		player.PlayerGui.MainGui.linkProfile.Visible = true
		return
	end
  • If the request failed (e.g., server offline, no internet), it gives the player an update button to let them try to update their status again.

	local data
	local decodeSuccess, decodeError = pcall(function()
		data = HttpService:JSONDecode(result)
	end)
  • This decodes the raw JSON string from the backend into a usable Lua table.
  • Again, wrapped in pcall in case the data is invalid/corrupt.

	if not decodeSuccess then
		warn("JSON decoding failed:", decodeError)
		return
	end
  • If the JSON couldn’t be parsed, the function stops here.

	if data.error then
		warn("Spotify API error:", data.error)
  • Spotify might return an error like invalid_token or no_active_device.
  • If so, this logs the issue and stops further UI updates.

	elseif typeof(data.track) == "string" and typeof(data.artist) == "string" then
  • Validates that the track/artist values are present and properly formatted.
  • Ensures you’re not trying to update the UI with bad or missing data.

		if request == "cre" then
			createDisplay(player, data)
  • If the request was "cre", it calls createDisplay() to attach a new GUI to the player.

		elseif request == "upd" then
			local gui = player.Character:FindFirstChild("Head")?.SpotifyDisplay
			if gui then
				updateDisplay(player, gui:FindFirstChild("Frame"), data)
  • If the request is "upd", it finds the player’s existing UI and refreshes the text based on the new data.
  • Uses optional chaining (?.) to safely access the child.

			else
				warn("No display to update")
			end
		else
			warn("Invalid request type")
		end
  • Handles fallback if:

    • There’s no UI to update.
    • Or the request was neither "cre" nor "upd".

		player.PlayerGui.MainGui.update.Visible = true
	else
		warn("Invalid or empty data received.")
	end
end
  • If all went well, it makes the Update button visible.
  • If the track or artist data is missing or corrupt, logs a warning.

Summary

This function does four big things:

  1. Fetch current Spotify data via the external API server.
  2. Parse the JSON response safely.
  3. Verify the data is valid and meaningful.
  4. Display or Update a 3D GUI inside Roblox for that player.

It’s built to fail silently.


Data Flow

[Player joins game]
       ⬇
[Client fires RemoteEvent]
       ⬇
[Roblox Server fetches /spotify/status/:id]
       ⬇
[The Node.js server calls Spotify API]
       ⬇
[Returns JSON: {track, artist, progress_ms...}]
       ⬇
[Roblox creates/upates GUI + starts timer loop]

In Action

UI


VIDEO – Music is a little strange… bare with me I love all music🙏


NOTE:

This was all done free, so feel free to try it out yourself if you’d like! though you’ll also be limited by Spotify API unless you’re an actual business owner… anyway. let me know if you try it yourself!!!

I never finished the game fully as I lost all motive once I found about the API restrictions. Even in the video, though cut out, it is bugged, all in roblox though, as the code was not fully fool proof. linking accounts then playing a song, then unlinking, then relinking, will cause error once the song ends on the games side with the timer. The timer is to blame as its not well put together. This is not the only issue though, there are plenty more, as of any game made. But if this had actually worked as indented for a large scale game, I would have made everything to perfection. But as of now, I end this project here. I hope you found this as fascinating as I did, I loved every second of putting this all together start to finish, its a shame I end it.

If you have any questions feel free to ask! im more than willing to answer!
As of posting:
image
Everything by: @X70PA

6 Likes