Tune - A Spotify browser and player

Hello,

Over the last couple of months I started writing a browser-based Spotify interface that fits the way I use the platform.

The UI is written in LiveView with a sprinkle of custom Javascript for specific widgets, while CSS is completely custom on top of the default milligram.css included with a generated Phoenix application.

I’m sharing it here to gather some feedback around structure and patterns.

LiveView structure

So far I have one main view (the explorer) which mounts a component (the bottom mini player) which mounts another component (the song progress bar). This hierarchy is the result of experimenting with the aim to minimize the amount of data sent over the network.

ExplorerLive
  |_ MiniPlayerComponent
     |_ ProgressBarComponent

Specifically: the “currently playing status” is updated every second according to these rules:

  1. A song change concerns the entire UI, as the currently playing song is highlighted in a few different places (e.g. if you’re browsing the album that contains the song).
  2. Playing, pausing, volume or device changes should update the entire miniplayer
  3. Progress in the same song only updates the progress bar

Implementation-wise, this translates to:

  1. and 2. Update relevant assigns in ExplorerLive and re-render the entire central portion of the UI and miniplayer
  2. Send updates to ProgressBarComponent and re-render the progress bar

Components are stateful, but not isolated process-wise.

Things I’m unsure about:

  • For case 3. the live view directly addresses the progress bar, bypassing the mini player component. This works, but I do wonder if it’s a case of implementation leak.
  • All event handling is in the live view. Again, seems like an implementation leak.
  • I’m finding directly managing assigns error-prone: I’d like to use less keys and have specialized functions to update the assigns state, but that makes it more difficult to control diffing during updates (i.e. more UI is re-rendered).

Session state machine

I’ve implemented the main connected user session as a separate state machine.

The implementation takes care of managing initial authentication, refreshing credentials, periodically polling new information and auto-termination in case a user has disconnected.

It’s implemented outside the live view session so that a user can have multiple browser windows open with only one session (and only one polling lifecycle).

I’m fairly happy about the structure so far - once you know the rules behind events in gen_statem, it looks reasonable to me but I’d like a second opinion. The code documentation has a diagram/description on how it’s implemented.

Here’s where I’m doubtful (lifted from the module docs)

Broadcast and subscribe are implemented via Phoenix.PubSub, however the
Worker maintains its own set of monitored processes subscribed to the session
id.

Subscription tracking is necessary to implementing automatic termination of a
worker after a period of inactivity. Without that, the worker would
indefinitely poll the Spotify API, even when no client is interested into the
topic, until a crash error or a node reboot.

In practice, a live view session subscribes to the worker process. The worker monitors the live view session, so that it can decide if it’s safe to terminate. Two browser windows for the same user equal two monitors.

Having a custom monitoring logic makes it that Phoenix.PubSub is almost redundant. I could message those processes directly, plus in a distributed scenario I would still need to update the state machine work correctly.

Testing

I’m experimenting with tests seeded with stream_data and expressed as properties. This suits well specific sections of the UI, e.g. search, where having randomized search results already helped me find issues I hadn’t thought about.

Properties, however, slow down (non-linearly) when I increase the number of runs. I still have to investigate and see where the problem is (e.g. shrinking gets slower or interaction with Mox is bottlenecked) - I’m wondering if by looking at the tests structure anyone with more experience in combining stream_data and Mox can shed some light. If not, I’ll dig and report back what I find.

General information

Thanks for reading so far!

20 Likes

Congrats on the project! This looks amazing in all aspects: UI, functionality and engineering. The documentation is also very well put together, solid work here, sir.

The discussions you bring up are a bit intricate and require some more time spent studying the code so I’ll come back to them afterwards, but I wanted to come give my first impressions of using the product as I listen to Spotify at least an hour a day.

The overall usability is very nice, however I would suggest against relying on LiveView events for the player (or at least the player UI), instant feedback in this crucial part of the app is more important so relying back to client-side JS will make the player feel more fluid I think (pausing audio should be instant, for example).

I would love to see the same treatment of displaying related albums and compilations given to not just Release Radar but any other playlist in my library (I have like 30 playlists). The top albums is a great view, I don’t know why the Spotify client hides this sort of information. As for the integrations, really liked the link back to last.fm, I wonder how much data could be displayed from them back into the app. Also, how about a lyrics page link for the songs as well? :wink:

Anyway, very interested in this project and hope to find some time later to dig deep into the code which from what I’ve skimmed over looks very top tier, it looks to me like the best example of a LiveView-OTP integration that is currently out there, congrats again!

1 Like

Congrats on the project! This looks amazing in all aspects: UI, functionality and engineering. The documentation is also very well put together, solid work here, sir.

Thanks, really appreciate your kind words.

The discussions you bring up are a bit intricate and require some more time spent studying the code so I’ll come back to them afterwards, but I wanted to come give my first impressions of using the product as I listen to Spotify at least an hour a day.

Yeah and I appreciate you taking the time doing that - I’m also inclined to think that it’s going to be a “pick your tradeoffs” scenario more than a “here’s the correct thing to do”.

The overall usability is very nice, however I would suggest against relying on LiveView events for the player (or at least the player UI), instant feedback in this crucial part of the app is more important so relying back to client-side JS will make the player feel more fluid I think (pausing audio should be instant, for example).

You’re spot on and I’ve been wondering about this myself and haven’t come to a conclusion yet. For premium accounts, you could use the in-browser SDK that Spotify provides, but only on supported browsers. So if you open the application on your phone and wish to control another device, the connect API is the only way to go.

So one could privilege the native browser SDK when available, fall back to the current implementation for other browsers.

The other direction which I haven’t investigated yet is to have optimistic UI updates that play nicely with information auto-refresh.

A naive implementation would work along these lines: you press the pause button, update the UI, send the event to the server and then to Spotify. Unfortunately, this doesn’t work as expected:

  • the state machine can broadcast a status update at any point in the sequence above, triggering an UI update with obsolete information. Only when receiving a subsequent update the player state will converge correctly.
  • Spotify’s API responses only acknowledge the operation attempt, i.e. when you send an API call to pause, the 200 response comes before the player actually pauses.

I’d personally be inclined to try and solve the problem tackling this second direction, because it’s more generic and applicable to all devices. In addition, if well done it can capture all interstitial states (e.g. trying to pause a song). In some ways, this scenario is a generalization of what’s summarized at https://hexdocs.pm/phoenix_live_view/form-bindings.html#javascript-client-specifics.

I would love to see the same treatment of displaying related albums and compilations given to not just Release Radar but any other playlist in my library (I have like 30 playlists).

I have some ideas around playlists (see https://github.com/fully-forged/tune/issues/50) but nothing along these lines - worth considering though.

The top albums is a great view, I don’t know why the Spotify client hides this sort of information.

I think by design Spotify wants to push people towards single tracks, rather than albums - I’m saying this considering their entire philosophy and business model (see https://musically.com/2020/07/30/spotify-ceo-talks-covid-19-artist-incomes-and-podcasting-interview/).

As for the integrations, really liked the link back to last.fm, I wonder how much data could be displayed from them back into the app.

This is in line with what I have in mind in terms of integrations - see https://github.com/fully-forged/tune/projects/3

Also, how about a lyrics page link for the songs as well?

My initial research in that area shows that lyrics require using paid services which are simply out of my budget - but I may be missing out.

Anyway, very interested in this project and hope to find some time later to dig deep into the code which from what I’ve skimmed over looks very top tier, it looks to me like the best example of a LiveView-OTP integration that is currently out there, congrats again!

Again, appreciate your words!

2 Likes

Thank you for sharing your project. I think it looks really interesting and provides a good starting point for these discussions :slight_smile:

I will just drop in two comments:

I agree that using Phoenix.PubSub here could be considered redundant. However, it will depend on how you go about solving the distributed case. Also,testing for side-effects that trigger messages might be easier with PubSub. This will depend on how the tests are structured though, so this may not apply.

I’ve played around a bit with Spotify integrations, and found that the Spotify Web Playback SDK pulls in Google Analytics, with cookies and all that jazz. That made in an instant no-go for me. I would recommend going with the optimistic UI approach - I believe this is also how the native Spotify apps work.

1 Like

Thank you for taking the time to reply!

I agree that using Phoenix.PubSub here could be considered redundant. However, it will depend on how you go about solving the distributed case. Also,testing for side-effects that trigger messages might be easier with PubSub. This will depend on how the tests are structured though, so this may not apply.

For the distributed case other work needs to be done, specifically how guarantee only one named user session process per cluster (e.g. having node-wide registration or consistent hashing to the same node). If you solve that, you can avoid pubsub because you effectively operate with same constraints as single node.

Test-wise, at this stage tests don’t use PubSub.

I’ve played around a bit with Spotify integrations, and found that the Spotify Web Playback SDK pulls in Google Analytics, with cookies and all that jazz. That made in an instant no-go for me. I would recommend going with the optimistic UI approach - I believe this is also how the native Spotify apps work.

At this point, the application already loads the Player SDK to have the in-browser audio player on supported browsers/devices. I can make that optional, so that when you login you can choose to turn it off (your point about analytics being loaded is sound).

1 Like

@pedromtavares I added minimal support for lyrics in the shape of a link to musixmatch.com, shown in an album tracklist. It works most of the times, because it “guesses” the specific track URL for the song.

It’s deployed, the merged PR with the implementation at https://github.com/fully-forged/tune/pull/94.

Looks good! Perhaps linking to a search results page using artist & track name would make for better results overall? Hard to pinpoint on this without a proper API, but something is definitely better than nothing.

Have a few things lined up before I get more into this project, but I thought of a feature idea that we could flesh out for me to work on if it interests you.

How would you feel about having users send in a playlist ID to be featured on the website? These playlists could then be voted on by other users and we could sort of keep a ranking that would just live on the sidebar. Not sure how much traffic or public interest you want this app to have or if it’s just a study project that should stay “invisible” to most people, but I there are definitely avenues to explore on this path of being a Spotify “companion” app.

Thank you! Yeah that would work as a fallback. It would also be possible to hide all of this behind a controller action that performs different attempts until it find something that works, so that the user doesn’t even see it.

I do appreciate the offer, but I do see this as a single user application - which would exclude competitions or ranking. To give you an idea of the long term scope I have in mind, see https://github.com/fully-forged/tune/projects/3.

I don’t have ambitions of growth or scale for the time being. It remains a project I work on in some spare time, with some experimentation and as much as possible without any pressure or stress. I want to try and extract some educational content out of it, but that’s as far as it goes.