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:
- 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).
- Playing, pausing, volume or device changes should update the entire miniplayer
- Progress in the same song only updates the progress bar
Implementation-wise, this translates to:
- and 2. Update relevant assigns in ExplorerLive and re-render the entire central portion of the UI and miniplayer
- 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
- You can use the application at tune.fullyforged.com (with no uptime guarantees, but it’s my production instance).
- Source at GitHub - fully-forged/tune: A streamlined Spotify client and browser with a focus on performance and integrations. · GitHub
- Code documentation at https://tune-docs.fullyforged.com
Thanks for reading so far!
Most Liked
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
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? ![]()
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
Thank you for sharing your project. I think it looks really interesting and provides a good starting point for these discussions ![]()
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.
Popular in Discussions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance









