Syncing Apple Music with Spotify
Before Apple Music launched in April 2015 I was a longtime Spotify user and subscriber. I maintained a playlist I affectionately called Ben Dodson’s Definitive Hits Collection which contained nearly 45 hours of songs I thought were particularly good1. On most Tuesday nights, my friend and podcast co-host John Wordsworth and I play a few rounds of Rocket League and we will regularly have the Definitive Hits on whilst we play. There are two issues with this:
- As I use Apple Music now, I don’t pay for a Spotify premium account and so I have to put up with adverts (which are utterly terrible).
- They aren’t in sync so we might be humming (or badly singing) along to a song that the other person isn’t listening to.
Now I could just recreate the playlist in Apple Music to solve the Spotify ads issue but we still wouldn’t be in sync. As we’re both developers, we decided to remedy this problem with a fairly convoluted solution…
The basic idea is that John acts as the host with the playlist on Spotify (on macOS) playing into his headphones. He has written an app that checks if the track has changed and, if it has, sends the track information to my server. I then use the iTunes Search API to look up the song and find the correct identifier which is then sent to an app on my iPhone via push notification to start the song playing on Apple Music.
I’ll run through each piece and go over the challenges that were encountered.
Retrieving track details from Spotify
I hadn’t heard of it before but Apple has provided a tool called Scripting Bridge since macOS 10.5 which allows you to interface with AppleScript from other programming languages such as Python and Ruby. With this, John was able to write an app that polls Spotify regularly2 to see if the track has changed. If a change is detected, the title, artist name, and album name are all sent to my server so I can begin the process of matching the song on iTunes. In future, we may add more information (track number on the album, duration, etc) in order to try and match better but this is working well enough currently.
Finding a track on Apple Music
The next step is for the server to take the information that has been sent and use the iTunes Search API to try and find a match. This is fairly straightforward and a first draft would send a request like this:
https://itunes.apple.com/search?entity=song&term=never+gonna+give+you+up+rick+astley&country=gb
Unfortunately the iTunes API does not allow you to search multiple terms (i.e. artist=rick+astley title=never+gonna+give+you+up
) so everything has to be concatenated together which leads to an issue; sometimes the song you expect is not the one you get. For example, consider the song She Looks So Perfect by “5 Seconds of Summer”; If you search for this, the first result on the iTunes API will actually be a the “Ash Demo Vocal” version of the song which is not the one we want. To resolve this, we started sending the album information (in addition to title and artist name) so I could match that manually by iterating through the results; I then only choose the first result if there isn’t a song in the list with the same title and album name.
The next issue I encountered involves the Romanization of Belarusian; the track Solayoh is rightly attributed to Alyona Lanskaya on Spotify but Apple Music uses Romanization so it becomes Alena Lanskaya. If you search term=solayoh+alyona+lanskaya
then you get no results. To fix this issue, if no results are returned from the iTunes API then I then do a search for the title alone and return the first result as that works in 99% of cases.
The final issue on the API side revolves around remastered tracks. The song A Horse with No Name is listed as A Horse with No Name - 2006 Remastered Version on Spotify but Apple Music doesn’t include that suffix even though they have the exact same version of the album. To fix this, if there are no results returned (again) then I split the string by non-alphanumeric characters and just try the first part in the lookup. Again, this seems to work in 99% of cases.
Once I have a track, I take the identifier and send it within a push notification along with the server time before I started making API calls (you’ll see why shortly). I use a silent push notification via the content_available
flag as I want to wake the app up and run some code but not actually display anything to the end user.
The iOS App
The final piece of the puzzle is an iOS app with a fairly minimal interface3…
The key thing for the iOS app to do is to play the track that comes from the push notification. This is fairly easy with an MPMusicPlayerController
but we run into problems when the app is in the background as whilst the app will wake up from the silent push it isn’t allowed to play music.
That said, we can enable the background audio capability that allows us to control audio from the background but it only works if audio is already playing. To remedy this, I play a 5 minute track from the album “Silent Tracks of Various Useful Lengths” (id #366737838) on repeat so that the app is continuously playing music… it just happens to be silent music4.
Once a silent push is received, it starts to play the track but it also adds the 5 minute silent track to the queue. This is important as it prevents the background audio from terminating should I have a different length of music to Spotify or if a push is delayed due to network reasons. In essence, a normal track will be played followed by a track of silence whilst it waits for the next notification.
The final issue to solve is one of latency; there is a lot of latency inherent in this setup as we are polling Spotify, sending data to a server, doing one or more lookups against the iTunes API, relying on a push notification, and finally buffering the song in Apple Music! In order to keep us roughly in sync, the app will connect with my server when enabled and fetch the server time so that it can keep time5. When the push notification comes in, it contains a timestamp from when the server was first hit by the macOS app and I can then calculate the offset in order to skip into the track a bit.
For example, lets suppose John starts listening to C’est La Vie by B*Witched6 and his app hits my server at 1485359762 seconds from the unix epoch. This is recorded and sent in the push notification along with ID #298026101. If that process takes 3 seconds, then the iPhone app will know the server time is now 1485359765 and can work out that it needs to skip forward 3 seconds in the song in order to keep me in sync.
Amazingly, this crazy system actually works and we are able to have our playlist synced and ad free on two completely separate streaming services. I built my portion of the project as an iOS app as Windows does not have access to Apple Music yet I play Rocket League on the PC; in order to actually hear the audio, I wear a single AirPod in my right ear underneath my Turtle Beach X10 Headset so I can hear the music but still get the audio from the game and Skype.
It was only after we’d got it working that we realised we could have just set up some form of streaming radio server but that likely wouldn’t have been as much fun…
-
The actual criteria to add songs is simple; I either have to use the phrase “it’s a classic” to be able to describe the song or it has to be “catchy as f**k”. ↩︎
-
An improvement would be to hook into some sort of notification so that the app can be told when Spotify changes track rather than polling every second but this works well enough for now. ↩︎
-
The switch simply activates the app as I’m using various background modes and don’t want my phone to randomly start playing music if John is listening to Spotify whilst we aren’t gaming! ↩︎
-
I was originally planning on using the track 4′33″ until I found the album of silent audio. ↩︎
-
Originally I would get the timestamp and then start counting it up with an uptime C method. This had some issues when the device was in standby so I made it simpler and I just work out the offset between the system clock and the server date; then, when I want to know what time it would be on the server, I can just add the offset to the system clock. ↩︎
-
It’s a classic. ↩︎