GSoc Proposal for Integrating Apple Music with ListenBrainz

Personal Information

Name: Vardan Saini
School: University of Alberta
Github: https://github.com/vardansaini
Linkedin: https://www.linkedin.com/in/vardansaini/
Portfolio: Vardan Saini
IRC nickname: vscode
email: vardan1@ualberta.ca

My contributions:

Project Overview

By linking their Apple Music account with ListenBrainz, individuals are able to use ListenBrainz to play their music selections. The integration into BrainzPlayer allows for this capability. By utilizing a content resolver, we can match the MusicBrainz Identifier (MBID) to the Apple Music catalog, permitting users to transfer playlists to their Apple Music account.

Introduction

ListenBrainz currently supports integration with Spotify, but there is a growing demand for integration with other music services. Apple Music is a popular music streaming service, and integrating it into ListenBrainz will provide more options for users to access personalized recommendations. This proposal outlines the steps to integrate Apple Music into ListenBrainz.

OAuth Integration

Frontend

  1. The first step in integrating Apple Music into ListenBrainz is to configure OAuth on the frontend. The MusicKit
    JavaScript library can be used to authenticate users with their Apple Music accounts using the OAuth protocol.
    <script src="https://js-cdn.music.apple.com/musickit/v3/musickit.js" data-web-components async></script>
    
  2. The developer token needs to be generated on the backend and sent to the frontend. Once the MusicKit library is
    included in the web page, the configure method is called to configure an instance of MusicKit on the Web.
    document.addEventListener('musickitloaded', async function () {
        try {
            await MusicKit.configure({
                developerToken: 'DEVELOPER-TOKEN',
                app: {
                    name: 'ListenBrainz',
                    build: 'GIT-COMMIT-SHA',
                },
            });
        } catch (err) {
            // Handle configuration error
        }
        const music = MusicKit.getInstance();
    });
    
  3. The authorize method can be used to authenticate the user and access their Apple Music account. This step involves
    the following:
    music.authorize().then(musicUserToken => {
        submitMusicUserTokenToLB(musicUserToken);
    });
    

Backend

  1. The music services callback URL (/music-services/{service}/callback) receives the authorization code in case of
    of other music services. However, in the case of Apple Music there is no client secret oauth available. Instead we
    do OAuth client side using MusicKit and once the user has granted access, the Music-User-Token is sent to server at
    callback URL.

  2. Apple Music’s catalog varies with the user’s storefront so store we should retrieve and store it.

    def get_storefront(music_user_token):
        headers = {'Authorization': f'Bearer {music_user_token}'}
        response = requests.get('https://api.music.apple.com/v1/me/storefront', headers=headers)
        return response.json()['data'][0]['id']
    
  3. The token and storefront can be stored in the database to be used later.

    @profile_bp.route('/music-services/apple/callback')
    @login_required
    def apple_callback():
        service = AppleService()
        music_user_token = request.args.get('music_user_token')
        service.get_storefront(music_user_token)
        service.add_new_user(current_user.id, music_user_token, storefront)
        return redirect(url_for('index'))
    

Integrate Apple Music in BrainzPlayer

The BrainzPlayer is a web-based player used to play music on ListenBrainz from various services. It already supports playing music from Spotify, Youtube and Soundcloud.

  1. MusicKit can be loaded on the page in the same as shown in the OAuth Frontend section.

  2. Once the player is loaded, we can use MusicKitInstance.{play,pause}
    etc. controls and add callbacks to playbackStateWillChange and playbackStateDidChange events to integrate with BrainzPlayer controls.

    music.addEventListener('playbackStateWillChange', ({ oldState, state }) => {
      console.log(`About to change the playback state from ${oldState} to ${state}`);
    });
    

    Before adding the event callbacks, we should check whether the user has authorized Apple Music.

  3. Tracks can be enqueued in the MusicKit player using setQueue or playNext method and its Apple Music Identifier:

    await music.playNext({ song: '1561837008' });
    
  4. If the listen does not have an Apple Music Identifier in it, we can query the search API as follows:

    const results = await music.api.search(term, {
        types: 'songs',
        limit: 10,
        storefront: 'us'
    });
    

    Then we can enqueue the desired track from the search results in the player and play it.

Build Content Resolver

The metadata content resolver is used to store metadata of the entire catalog present in an external service’s database.
Currently, ListenBrainz has such a resolver for Spotify. As a part of this project, I intend to add a similar content resolver for Apple Music as well.

  1. Use the Apple Music API to get all storefronts (https://api.music.apple.com/v1/storefronts)

    [
        {
            "id": "dz",
            "type": "storefronts",
            "href": "/v1/storefronts/dz",
            "attributes": {
                "explicitContentPolicy": "opt-in",
                "name": "Algeria",
                "defaultLanguageTag": "en-GB",
                "supportedLanguageTags": [
                    "en-GB",
                    "fr-FR",
                    "ar"
                ]
            }
        },
        // more storefronts ....
    ]
    
  2. Get catalog charts for each storefront to seed metadata resolver (https://api.music.apple.com/v1/catalog/us/charts?types=songs,albums)

    {
        "results": {
        "songs": [
            {
                "chart": "most-played",
                "name": "Top Songs",
                "orderId": "most-played:songs",
                "next": "/v1/catalog/us/charts?chart=most-played&genre=20&limit=1&offset=1&types=songs",
                "data": [
                    {
                        "id": "635016640",
                        "type": "songs",
                        "href": "/v1/catalog/us/songs/635016640",
                        "attributes": {
                            "albumName": "I Love You.",
                            "composerName": "Jesse, Zachary Abels & Jeremiah Freedman",
                            "playParams": {
                                "id": "635016640",
                                "kind": "song"
                            },
                            "name": "Sweater Weather",
                            "artistName": "The Neighbourhood",
                            // ... more fields
                        }
                    }
                ],
                "href": "/v1/catalog/us/charts?chart=most-played&genre=20&limit=1&types=songs"
            }
        ],
        "albums": [
            {
                "chart": "most-played",
                "name": "Top Albums",
                "orderId": "most-played:albums",
                "next": "/v1/catalog/us/charts?chart=most-played&genre=20&limit=1&offset=1&types=albums",
                "data": [
                    {
                        "id": "1608118564",
                        "type": "albums",
                        "href": "/v1/catalog/us/albums/1608118564",
                        "attributes": {
                            "playParams": {
                                "id": "1608118564",
                                "kind": "album"
                            },
                            "name": "mainstream sellout",
                            "artistName": "Machine Gun Kelly",
                            // .. more fields
                        }
                    }
                ],
                "href": "/v1/catalog/us/charts?chart=most-played&genre=20&limit=1&types=albums"
            }
        ]
    }
    }
    

    Using the ids from this data, seed the content resolver.

  3. Get all albums of the artist (https://api.music.apple.com/v1/catalog/us/artists/{artist_id}/albums)

    [
        {
            "id": "1482041821",
            "type": "albums",
            "href": "/v1/catalog/us/albums/1482041821",
            "attributes": {
                "copyright": "℗ 2019 Mom+Pop",
                "genreNames": [
                    "Alternative",
                    "Music"
                ],
                "releaseDate": "2020-02-14",
                "upc": "858275058666",
                "artwork": {
                    "width": 3000,
                    "height": 3000,
                    "url": "https://is3-ssl.mzstatic.com/image/thumb/Music125/v4/0b/b2/52/0bb2524d-ecfc-1bae-9c1e-218c978d7072/Honeymoon_3K.jpg/{w}x{h}bb.jpg",
                    "bgColor": "fffaa9",
                    "textColor1": "030005",
                    "textColor2": "363240",
                    "textColor3": "353226",
                    "textColor4": "5e5a55"
                },
                "url": "https://music.apple.com/us/album/honeymoon/1482041821",
                "playParams": {
                    "id": "1482041821",
                    "kind": "album"
                },
                "recordLabel": "Mom+Pop",
                "trackCount": 9,
                "name": "Honeymoon",
                "artistName": "Beach Bunny"
            }
        },
        // .. more albums
    ]
    
  4. Get all tracks of multiple albums (https://api.music.apple.com/v1/catalog/us/albums?ids={album_ids})

    {
        "data": [
            {
                "id": "1616728060",
                "type": "albums",
                "href": "/v1/catalog/us/albums/1616728060",
                "attributes": {
                    "name": "Heart On My Sleeve",
                    "contentRating": "explicit",
                    "artistName": "Ella Mai"
                },
                "relationships": {
                    "tracks": {
                        "href": "/v1/catalog/us/albums/1616728060/tracks",
                        "data": [
                            {
                                "id": "1616728064",
                                "type": "songs",
                                "href": "/v1/catalog/us/songs/1616728064",
                                "attributes": {
                                    "albumName": "Heart On My Sleeve",
                                    "genreNames": [
                                        "R&B/Soul",
                                        "Music"
                                    ],
                                    "trackNumber": 1,
                                    "durationInMillis": 197314,
                                    "releaseDate": "2022-05-06",
                                    "isrc": "USUM72203653",
                                    "artwork": {
                                        "width": 3000,
                                        "height": 3000,
                                        "url": "https://is3-ssl.mzstatic.com/image/thumb/Music112/v4/03/45/19/034519dc-9ff9-7f63-3d02-23a101a0cc3a/22UMGIM01299.rgb.jpg/{w}x{h}bb.jpg",
                                        "bgColor": "0b1b18",
                                        "textColor1": "fc977a",
                                        "textColor2": "ef5429",
                                        "textColor3": "cc7e66",
                                        "textColor4": "c14925"
                                    },
                                    "composerName": "Charles Hinshaw Jr, Dijon McFarlane, Ella Mai Howell, NICHOLAS MATTHEW BALDING, Peter Johnson & Shah Rukh Zaman Khan",
                                    "playParams": {
                                        "id": "1616728064",
                                        "kind": "song"
                                    },
                                    "url": "https://music.apple.com/us/album/trying/1616728060?i=1616728064",
                                    "discNumber": 1,
                                    "artistName": "Ella Mai"
                                }
                            },
                            // ... more tracks
                        ]
                    }
                }
            },
            // ... more albums
        ]
    }
    
  5. Store data in a normalized tables in a database schema similar to spotify content resolver and set a cache expiry.

  6. Before retrieving data from API, check if it exists already in database and update cache accordingly.

Timeline

Week 1-4: Integrating Apple Music in BrainzPlayer (without OAuth), setting up controls, searching tracks and playing previews

Week 5-7: Getting Music User Token, storefront, storing it in database

Week 8-11: Build the content resolver to store Apple Music catalog metadata in ListenBrainz

Week 12-13: Buffer Period

Other Information

Tell us about the computer(s) you have available for working on your SoC project!

I have a Macbook pro 16 inch with 2.3 GHz 8 core intel i9 (9th gen)
, AMD Radeon Pro 5500M 4 GB, 16 GB 2667 MHz DDR4 and 1 TB SSD.

When did you first start programming?

Since 2011, I’ve been involved in programming using Java, and over time, I’ve had the opportunity to work on numerous projects.

What type of music do you listen to? (Please list a series of MBIDs as examples.)

I listen to Punajbi, English and Hindi music.
Here are MBID’s of some of my favourite artists:-

Name: Karan Aujla
MBID: 4a779683-5404-4b90-a0d7-242495158265

Name: Sam Smith
MBID: 5a85c140-dcf9-4dd2-b2c8-aff0471549f3

Name: Imagine Dragons
MBID: 012151a8-0f9a-44c9-997f-ebd68b5389f9

What aspects of the project you’re applying for (e.g., MusicBrainz, AcousticBrainz, etc.) interest you the most?

Music is an essential part of my life, and I listen to it while doing everything. It is particularly crucial for me when I work out at the gym, which I do daily. As a software engineer who works on computer allot, I rely on Apple Music, and I wanted to integrate it with Listenbrainz, which supports various other platforms. Therefore, this project is a perfect blend of my favorite things.

Have you ever used MusicBrainz to tag your files?

Not much but I have heard allot about it from my friends at univeristy.

Have you contributed to other Open Source projects? If so, which projects and can we see some of your code?

I have previously made contributions to ListenBrainz, as well as to an open-source project called Capstone Dashboard which is available on GitHub at Capstone-dashboard-open-UofA(https://github.com/open-uofa/capstone-course-dashboard).

What sorts of programming projects have you done on your own time?

I am conducting NLP research utilizing transformers, and have also applied my knowledge in PCGML and unity to develop a no-code platform for creating 2D games without programming. In addition, I have completed contract work involving MERN techstack to deliver websites, created my own portfolio using React, and have dabbled in Flutter to develop apps for leisure. Additionally, I have experimented with Flask. For more examples of my work, please visit my Github profile.

3 Likes

Hi!

Thanks for the proposal. It is detailed and shows good research of the various APIs that need to be used in the project. I can think of 2 gaps in it:

  1. In spotify, each track is associated with a list of regions (markets) in which the track is available. But to query a track we do not need to specify a region. This is not the case for Apple Music, where the region (storefront) is mandatory to query any catalog resource. It will require a bit of investigation to work out how much the catalogs vary across regions, whether the same track has different ids in different regions etc. Depending on the result, we may need an extra storefront column or an extra relation table in the normalized schema.

  2. The Music-User-Token can be retrieved from MusicKit and sent to the backend but there is no way to refresh the token. MusicKit’s documentation hints that these tokens are expiring which kind of puts us in a spot of bad UX. The documentation doesn’t mention how long the tokens last though so again it will need some investigation to proceed. Further, @mr_monkey needs to be involved in this discussion and he won’t be around for a few days.

Therefore, I guess its fine to leave these both aspects open-ended for now. But if you do have some thoughts, feel free to add those in your proposal.