Home Category
blurhashcover

I made a web application to show my Apple Music in my GitHub profile (or anywhere)

Written by Phumrapee Limpianchop on 13 Oct 2022 · 11 min read

Development

This is a companion article from Going even further than the Serverless platform, but it is supplemental if you are only interested in the features of this project.

Jacquard RTC

During the past few months, I am forcing myself to learn all the development kits that Apple publicly provided. From peer-to-peer communication with MultipeerConnectivity (featured in Jacquard RTC), Map markers and location services with MapKit (featured in Taobin Sighting), to new live activity card and Dynamic Island with ActivityKit (featured in Jacquard RTC, and swift implementation of Riffy H).

Why? Because developer subscriptions, MacBook, and iPhone are expensive, I buy all features so I have to use all features.

Well, all of those are written purely in Swift, a programming language invented by Apple, but they also have development kits for server-side works and web browsers as well so I would like to pick something up for a project.

Part 1: Inspiration

After thoroughly searching Apple documentation, I found MusicKit to be the perfect candidate to try. Not only does Apple already have Swift library and JavaScript client for browsers, but they just release Apple Music API for server-side backend in recent WWDC 2022 as well.

Now I know what to use, but how about what to make? A web music player???

Nah, Apple already made that how could public API beats internal private API. Then it hit me, I saw P’Kittinan GitHub profile and found this interesting Spotify card that shows recently played song.

github-kittinan

This is a project made by him with a neat name called spotify-github-profile. I don’t know how it works but my guess is you connect your Spotify account to his Application, and you get a string of URL to dynamically generated SVG with the user ID to identify each specific user.

So, I make a journey to make this project but using Apple Music instead. Now, normally each project that I try is completely new stuff I usually finished within one weekend but turns out I underestimated this project by a big margin. Well, let’s talk about those.

Part 2: Getting access to Apple Music API

Now, this is the most confusing entry point for beginners because Apple very poorly described in their documentation how to authenticate.

First, Apple tells you to create a key. To do that you have to create a Media ID identifier with MusicKit enabled. Then you will be able to create a Key with media services enabled and configured.

Then, you download the key back from Apple and saw that it’s a PEM private key, but now when you read the next section of the documentation they authenticate to Apple Music API with JWT Bearer header, with the minimal explanation on how to make one.

With a bit of more research, I learned that I have to sign the JWT token by myself with a provided private key. You have to use a private key to sign the token with the data inside containing the ID of Key that you’ve generated and your Team ID which can be seen in your Apple Membership status.

import jwt from 'jsonwebtoken'

...

const token = jwt.sign({},
  APPLE_PRIVATE_KEY.replaceAll(/\\n/g, "\n"),
  {
    issuer: APPLE_TEAM_ID,
    expiresIn: keyDuration,
    algorithm: 'ES256',
    header: {
      alg: 'ES256',
      kid: APPLE_KEY_ID,
    },
  }
)

Then you provide the token into the Authorization header, and… viola!

apple-music-api-storefronts

Now, you can interact with the API. The next step is to access user data.

Part 2: Authorizing user

I wish that I could easily authorize users within the same API but you can’t. The only way on the web to authenticate the user is to use MusicKit JS so your web will bloat by a bit for their JavaScript library to load. Then, I have to configure it with generated JWT token before I could authorize a user.

// configure immediately after script loaded
await MusicKit.configure({
  developerToken: 'DEVELOPER-TOKEN',
  app: {
    name: 'My Cool Web App',
    build: '1978.4.1',
  },
})

// when user click button then authenticate user
const onClick = () => {
  // get musickit instance
  const music = MusicKit.getInstance()

  // authorize user
  const userToken = await music.authorize()
  console.log({ userToken })

  // now use `userToken` will return a user authroized string
  // do anything with it
  ...
}

Now you have both the developer token and user token, then you can try calling some API on the backend that is related to user data. Let’s say a recently played song.

apple-music-api-user

Now we have all the data, let’s prototype this thing.

Part 3: Prototyping

So at first, I just prototyping with only my authentication token first.

But how could I dynamically generate SVG dynamically without pain, and be scalable so that I could implement more themes/styles of the card even further in the future?

The answer is EJS, the best templating engine in the world. So, I wrote an API to get the most recently played song then download album artwork and optimize it if necessary. Then I provide all of those as one metadata object which looks like this.

{
  title: '私をAKIBAにつれてって (feat. 鏡音リン)', // song title
  artist: 'Wonderful★opportunity!', // artist
  coverImageData: 'data:image/jpeg;base64,...', // base64 encoded of artwork
  timestamp: {
    percentage: '33.33', // percentage to seek scroller already elapsed
    elapsed: '1:38', // time stamp of duration already listened to
    remaining: '3:16' // time stamp of remaining time
  }
}

And now, I can place these values into the template easily.

<div class="cover-image-container">
  <% if (coverImageData !== null) { %>
	<img class="cover-image" src="<%= coverImageData %>" />
  <% } else { %>
  <div class="cover-image" />
  <% } %>
</div>

<div class="content">
  <h1 class="song-title text-gray-900 truncate"><%= title %></h1>
  <h2 class="song-artist text-gray-500 truncate"><%= artist %></h2>
</div>

After it is rendered, I optimize it a bit by removing some white spaces, and new lines then send it as a result.

res.setHeader('Content-Type', 'image/svg+xml')

const { optimize } = await import('svgo')
res.send(
  (optimize(ejs.render(templateFile, builtRenderedData)) as OptimizedSvg).data
)

And it’s working!!

prototype-svg

Part 4: Making it for everyone

Now, I have to scale that anyone can use this application…but there’s a problem.

You see, the only thing that I have from the user is User authorization token. It is supposed to be private if I made a URL to identify the user by authorization token that would be a huge security issue. I need some kind of UID (user identifier) that could be used publicly. The problem is Apple Music API doesn’t provide any (unlike Spotify API).

The solution to this problem is Sign In with Apple ID. I will require the user to authenticate with SSO first, then authorize Apple Music. And before you guys shout that why only using Sign In with Apple ID.

If you don’t have Apple ID, then how do you subscribe to Apple Music???

So, I have to go back to the developer portal to create App ID, and Service ID for Sign In with Apple ID capability enabled and configured. Then, connect this Service ID to the same key.

After many attempts, I’m able to get a user ID.

/**
 * Verify authentication code, and get authentication data
 */
const tokenResponse = await appleSignin.getAuthorizationToken(code, {
  clientID: 'com.rayriffy.apple-music.auth',
  redirectUri: 'https://apple-music-github-profile.rayriffy.com/api/auth/callback',
  clientSecret: clientSecret,
})
const { sub: appleUserId, email: appleUserEmail } = await appleSignin.verifyIdToken(
  tokenResponse.id_token,
  {
    audience: 'com.rayriffy.apple-music.auth',
    ignoreExpiration: false,
  }
)

console.log('user id: ', appleUserId)

This took me a week to understand their process, it’s unlike any OAuth that I used before.

Anyway, let’s deploy to Vercel and see it in action, and…wtf!?

card-vercel

Part 5: Making it faster

So, this is what I learned after putting it to the test in my GitHub profile.

  1. SVG images are proxied via GitHub camo
  2. Serverless function take ~1 second to boot
  3. Entire fetching process from getting data to downloading and optimizing album artwork took ~1.4 seconds
  4. By then camo already timed out

So, I have 2 problems to solve

  1. How to reduce the entire fetching time, both server-side and client-side?
  2. How to reduce load to not fetching every time?

So, there’re 2 solutions to this. Firstly, I redeploy my project from Vercel to Deploys.app which is a Container-as-a-service cloud provider. I repack the entire application into Docker images with image size squeezed down to about 120MB then configure to have at least 1 replica always on standby for requests and scale up to specific replicas on high load.

deploys-app-usage

Not only do I reduce fetching time on the client side by removing the entire process of the cold boot by 1 second, but also optimize code inside the application itself to utilize Promises as much as possible. Which reduces an overall fetching time for the client to sub-1-second.

Secondly, to reduce load to not fetching every time when a new request comes in. The answer to this is caching.

Now only to cache on the user’s local browser, but also on the shared cache as well. So, I configured it to store in the browser for 60 seconds, then after that, you will have to request a new image. Next, every finished request is stored in a shared cache on a server so the same cache now can be used across multiple browsers. This cache will be stored for 31 days, but after the image has an age of 128 seconds it will make a request on the server side to update its information.

tl;dr it’s stale-while-revalidate strategy

/**
 * Store in local browser for 60 seconds
 * Stored cache on server is fresh for 128 seconds
 * After that, cache on server still serveable for 31 days but it will trigger for a fresh update
 */
res.setHeader(
  'Cache-Control',
  `public, max-age=60, s-maxage=128, stale-while-revalidate=${
    60 * 60 * 24 * 31
  }`
)

As the result, now GitHub camo can obtain an image without crashing anymore!! 👏🏼👏🏼👏🏼

many-cards

Conclusion: What’s next?

Now what’s next, this project is open-source in my repository. This month is Hacktoberfest as well so why not contribute to this project by making more themes for others to use? That would be awesome. If you like this works, feel free to also start this project as well.

Also, if you want to flex your Apple Music library you can go to my website to try as well.

I will also make more themes when I’m not busy as well. But basically, the project is completed and there’re more developer kits from Apple to explore. So, I will keep updated on this blog if I found any API interesting to share with.

Also, huge thanks to P’Kittinan without his work this project would not comes to a reality.