cloud8421

cloud8421

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!

Most Liked

cloud8421

cloud8421

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 Form bindings — Phoenix LiveView v1.2.5.

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 Collect mode · Issue #50 · fully-forged/tune · GitHub) 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!

pedromtavares

pedromtavares

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!

trarbr

trarbr

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.

Where Next?

Popular in Discussions Top

Jayshua
I recently came across the javascript library htmx. It reminded me a lot of liveview so I thought the community here might be interested....
New
Donovan
Hello everyone, I’m so glad to have discovered this awesome community. Thanks for creating it! This is my second post, and apologies for...
New
mikl
I wanted to capitalize a string, and tried using String.capitalize(). That generally works well, until you try to capitalize a word like...
New
JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1144 53690 245
New
chuck
Let me start by stating an assumption: Phoenix is a great approach to building REST APIs. There are many reasons for this, but I will ass...
New
nburkley
AWS re:Invent is on at the moment with some interesting announcements. One new feature in particular is the Lambda Runtime API for AWS La...
New
ejpcmac
I have discovered Nix last month and I am currently on my way to migrating to it—both on macOS at home and the full NixOS distrubution at...
New
und0ck3d
Hello everyone! A few days ago I’ve created a topic here about how people were creating CMSs with Elixir and Phoenix. I’ve been studying...
New
AstonJ
If so I (and hopefully others!) might have some tips for you :slight_smile: But first, please say which area you’re finding most challen...
New
Owens
Hello all, I am developing a new mobile app with Flutter frontend and Phoenix backend. The mobile app has real-time task management and c...
New

Other popular topics Top

senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New
marick
I had some trouble figuring out how to make many-to-many associations work. Once I got it working, I wrote a blog post. Because I’m a nov...
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New
svb
Hi! Currently I want to submit a form by pressing the Enter key. However, since my input field is of type “textarea” this is just adds a...
New
sergio
Kind of like when jquery came out, it was super necessary. Existing drag and drop libraries have a bunch of baggage to support old browse...
New

We're in Beta

About us Mission Statement