Making a better mobile Spotify app: Part 2
Dammit, guys! I was kidding! You weren't actually supposed to actually takean houra weeka month! God, we've lost so much time. Let's just do this already!— Diabel, from SAO: Abridged
So, yeah, I took a small break. But now it's over so we can get back to developing Spotafly!
Yes, that's the name I chose for the project. No I won't be changing it. Also, for people who have been waiting for LightTube API refactor & OAuth, I'm sorry, my mind has constantly been occupied with this project for the past one and a half weeks.
Now, lets continue from where we left off.
Part 1: I wonder if I can steal some cookies from a WebView
Remember when I said that I wouldn't implement the login part of the app because it needed a captcha? Well, times have changed, and I've decided to come back to it.
Now, I provably can't show the user a captcha that Spotify will accept by writing kotlin code, so let's try something else. Introducing: the Android WebView
It's amazing how simple this is! You just give it a URL and it shows you the page- nope you have to manually enable JavaScript.
After enabling JavaScript, we're now at the fun part: logging in & snooping the cookie! Thankfully, you can execute JavaScript snippets in in the WebView & get the results back. So, lets just get the result of document.cookie
!
Ok, I looked into the Firefox network tab again, and the request to log in sends a Set-Cookie
header in the response. So if we can just intercept the request, we might just get the token in the response header, right?
Well, wrong! In the vanilla WebView client, you can intercept the request, but we will only get the URL, the method & the headers. But we only need the body.
A quick StackOverflow search lead me to a library that claims to actually let you get the body from the intercepted requests, but I'm not gonna add more dependencies into the project.
So, we can't get the cookies via JavaScript, or intercept the request. What do we do? Set up a whole HTTP proxy to get the request & the response?
No, you idiot! There's a class for literally getting the cookies from WebView! So we have to just pass the URL into a method from that class and boom!
There's the token! And it just works with my API client from the last post!
With that done, lets have some fun!
spoilers
This next section was in fact, not fun
oh by the way, while doing this, spotify actually made me reset my password :3
Part 2: Hey Spotify, look at me, I'm playing some songs
After a lot of research, I have found out how the Spotify players work. At least the web player.
What the web player does is connect to a WebSocket, which immediately gives us a "connection ID". And then it creates a device in the API using the same connection ID, which shows up in the "Play on Other Devices" menu. So, let's recreate that functionality.
After finding a WebSocket library in StackOverflow, it didn't take long for me to manage to create a device. Look, its right there!-wait, no, what?
Yup, it disappears after a few seconds... But why? Hell if I know.
So, I just said "fuck it", installed IntelliJ IDEA, and created a new project just to handle this websocket thing, which i will refer to as "the state manager" from now on. You'll see why later.
And after almost ripping all my hair off, I found the problem. The WebSocket library I was using was just disconnecting right after the initial connection was established.
Welp, time to pull out the ol' reliable OkHttp! (i really should've realized that it had websocket support before)
After switching to OkHttp for the WebSocket, making classes for the messages I receive, blah blah, I could finally stay connected! Now it was time to figure out when and how I receive the messages. I will list what each of them do here.
Replace State
This one basically contains the whole player state after you do anything in the remote controller. It contains a list of all tracks with their file IDs (there you are!), if the player is paused or not, shuffle/repeat status, if it should be seeked to somewhere, etc. This is also why the IntelliJ project I created is named the "State Machine"
Set Volume
I... don't think I have to explain this but here I go anyway. This one is sent after you change the volume in the remote controller. It has nothing else.
Cluster
This one gets sent a lot. It basically contains all the devices, what they're doing, the list of tracks that played before the current one, list of tracks that will play after this one and so on... I will just ignore this for now.
Liked songs/Followed artists
wait why does the player even need this??
With those, we could finally get the player queue, file IDs and stuff, and play the songs! (unfortunately I don't have any screenshots for this. you can have two screenshots of the latest state of the app tho)
Well, it works! not
After playing the first 10 seconds of any track, ExoPlayer just pauses with an error saying "Crypto key not available". What this means it that ExoPlayer is way too lazy to make the DRM request and stuff, or that's what I think is happening. However, it seems like this issue is fixed by just telling the player to continue playing.
So, how do you fix this problem? It is very hard to fix and only a Real Programmer:tm: could figure out. You will not believe how the fix works.
You just tell ExoPlayer to continue playing.
The only problem this introduces is a few seconds of pause when the DRM problem occurs. Outside that, works just like the stock Spotify app (minus the ads, inability to skip songs or not being able to not shuffle playlists). Ready for pushing to production?
Part 3: for real though, can I use this?
No.
The app is literally the definition of "shit code" right now. The only tolerable part is the API part and that's because I wrote 30% of it outside the actual Android project (the state manager), 60% of it was generated by QuickType (really cool site tbf), and the rest is literally like 6 methods to get the home page, playlist info etc.
Eventually, you will see this app on my GitHub, and only then you will be able to download and use it yourself.
Well, this is the last post I will make about Spotafly (or Spotify in general tbf). I will see y'all later when I decide to fuck around with another website.