How to get the most played Apple Music songs and albums using Swift

Sponsored

Codemagic logo
Codemagic CI/CD for mobile teams

What do you get when you put love for iOS and DevOps together? Answer: Codemagic CI/CD

It is that time of the year when music streaming services provide users with a recap of their most played songs and albums. If you are an Apple music user, you will know that the service is no exception and you’ll be familiar with its famous “Apple Music Replay” playlist that is frequently updated with your most played songs of the year.

But what if you are a developer and want to show the user’s most played Apple Music songs and albums in your app? Is that information accessible? The answer is yes but, as you will see in this article, it is not as straightforward as you might think.

MusicKit vs MediaPlayer

You can use one of two frameworks to retrieve information about the user’s music library: MusicKit and MediaPlayer.

While both frameworks grant you access to a large number of music library information, as it stands, there is no method or endpoint to retrieve a list of the user’s most played songs or albums in either framework. Instead, you need to manually get a list of all the songs or albums in the user’s library and then apply some manual logic to filter and sort the results.

Both frameworks can achieve the same result but while MusicKit is easier to use and requires less manual work from the developer’s side, it seems to be slightly slower than MediaPlayer in executing the queries.

For this reason, if you’re not too concerned about performance and value simplicity, I would thoroughly recommend using MusicKit, but if performance is important to you, worry not as I will show you how to use both frameworks in this article so you can decide which one is best for your use case.

Getting the most played songs

Let’s start by getting the most played songs in the user’s library for a given year using both frameworks.

Using MediaPlayer

MostPlayedService.swift
import MusicKit
import MediaPlayer
import CollectionConcurrencyKit

final class MostPlayedService {
    func getMostPlayedSongs(inYear year: Int, limit: Int = 5) async -> [Song] {
        // 1
        let query = MPMediaQuery.songs()
        guard let allSongs = query.items else { return [] }

        let topSongs = try? await allSongs
            // 2
            .filter { $0.mediaType == .music && $0.playCount > 0 && $0.lastPlayedDate?.year == year }
            // 3
            .sorted { $0.playCount > $1.playCount }
            // 4
            .prefix(limit)
            // 5
            .concurrentMap { await self.getSong(withID: $0.playbackStoreID) }
            .compactMap { $0 }

        return topSongs ?? []
    }

    private func getSong(withID id: String) async -> Song? {
        var songRequest = MusicCatalogResourceRequest<Song>(matching: \.id, equalTo: MusicItemID(id))
        songRequest.limit = 1
        songRequest.properties = [.albums]
        let response = try? await songRequest.response()
        return response?.items.first
    }
}

Let’s break down the code above step by step:

  1. First, get all the songs in the user’s library using the song static method on MPMediaQuery.
  2. Then, only keep the music items that have been played at least once in the given year.
  3. Sort the music items from most played to least played.
  4. Only after the sorting has taken place, keep the number of items specified by the limit parameter.
  5. Finally get the full information for the song using MusicKit and return the result. Note that I am using the concurrentMap method from John Sundell’s CollectionConcurrencyKit to execute all requests concurrently.

Using MusicKit

MostPlayedService.swift
import MusicKit

final class MostPlayedService {
    func getMostPlayedSongs(inYear year: Int, limit: Int = 5) async -> [Song] {
        // 1
        var songRequest = MusicLibraryRequest<Song>()
        // 2
        songRequest.sort(by: \.playCount, ascending: false)
        // 3
        let response = try? await songRequest.response()

        let mostPlayedSongs = response?
            .items
            .map { musicItem in musicItem as Song }
            // 4
            .filter { song in (song.playCount ?? 0) > 0 && song.lastPlayedDate?.year == year }
            // 5
            .prefix(limit) ?? []

        return Array(mostPlayedSongs)
    }
}

Let’s break down the code above step by step:

  1. First, create a MusicLibraryRequest for the Song type. This type of request does not query Apple Music’s catalog but instead queries the user’s library.
  2. Add a sort descriptor to the request to sort the songs by their playCount property in descending order.
  3. Execute the request and get the response.
  4. Filter the songs to only keep the ones that have been played at least once in the given year.
  5. As the request has a sort descriptor, we don’t manually need to sort the songs again. Instead, we can simply keep the number of songs specified by the limit parameter.

Getting the most played albums

Let’s now see how to get the most played albums in the user’s library for a given year using both frameworks. As you will see in the code snippets below, the process is slightly more complex than getting the most played songs as albums don’t have playCount properties and we need to calculate the play count for each album by summing the play count of all the songs in the album.

Using MediaPlayer

MostPlayedService.swift
import MusicKit
import MediaPlayer
import CollectionConcurrencyKit

final class MostPlayedService {
    func getMostPlayedAlbums(inYear year: Int, limit: Int = 5) async -> [Album] {
        // 1
        let query = MPMediaQuery.albums()
        guard let allAlbums = query.collections else { return [] }

        let mostPlayedAlbums = try? await allAlbums
            // 2
            .filter { $0.mediaTypes == .music }
            // 3
            .reduce(into: [String: Int](), { partialResult, collection in
                let wasAlbumPlayedInYear = collection.items
                    .map { $0.lastPlayedDate?.year == year }
                    .contains(true)

                guard let representativeItem = collection.representativeItem, wasAlbumPlayedInYear else {
                    return
                }

                let playCount = collection.items.reduce(into: 0) { partialResult, item in
                    if item.lastPlayedDate?.year == year { partialResult += item.playCount }
                }
                partialResult[representativeItem.playbackStoreID] = playCount
            })
            // 4
            .filter { _, playCount in playCount > 0 }
            // 5
            .sorted { lhs, rhs in lhs.1 > rhs.1 }
            // 6
            .map { songID, _ in songID }
            // 7
            .prefix(limit)
            // 8
            .concurrentMap { songID in await self.getSong(withID: songID) }
            // 9
            .compactMap(\.?.albums?.first)

        return mostPlayedAlbums ?? []
    }
}

Let’s break down the code above step by step:

  1. Similarly to the previous example with songs, first get all the albums in the user’s library using the albums static method on MPMediaQuery.
  2. Filter to only keep the music items.
  3. Reduce the albums to a dictionary where the key is one of the album’s songs’ playbackStoreID and the value is the sum of the play count of all the songs in the album in the given year. The reason for using a song’s playbackStoreID as the key is that the album media item does not have an id that can be used to fetch the full album information using MusicKit.
  4. Filter the albums to only keep the ones that have been played at least once in the given year.
  5. Sort the albums from most played to least played.
  6. Only keep the value from the dictionary, which is the song’s playbackStoreID.
  7. Only keep the number of albums specified by the limit parameter.
  8. Get the full information for the album’s song using MusicKit. Note that I am using the concurrentMap method from John Sundell’s CollectionConcurrencyKit to execute all requests concurrently.
  9. From the song’s information, get the album information if available and return the result.

Using MusicKit

MostPlayedService.swift
import MusicKit

final class MostPlayedService {
    func getMostPlayedAlbums(inYear year: Int, limit: Int = 5) async -> [Album] {
        // 1
        let response = try? await MusicLibraryRequest<Album>().response()

        let mostPlayedAlbums = response?.items
            .map { musicItem in musicItem as Album }
            // 2
            .compactMap { album -> (album: Album, playCount: Int)? in
                let playCount = album.tracks?.reduce(into: 0, { playCount, track in
                    if track.lastPlayedDate?.year == year {
                        playCount += track.playCount ?? 0
                    }
                }) ?? 0
                guard playCount > 0, album.lastPlayedDate?.year == year else { return nil }

                return (album: album, playCount: playCount)
            }
            // 3
            .sorted { lhs, rhs in lhs.playCount > rhs.playCount }
            // 4
            .prefix(limit)
            // 5
            .map(\.album) ?? []

        return mostPlayedAlbums
    }
}

Let’s break down the code above step by step:

  1. First, create a MusicLibraryRequest for the Album type. This type of request does not query Apple Music’s catalog but instead queries the user’s library.
  2. Reduce the albums to a tuple where the first element is the album and the second element is the sum of the play count of all the songs in the album in the given year. The reason for using a tuple is that the album information does not have a playCount property and it must be calculated manually.
  3. Sort the albums from most played to least played.
  4. Only keep the number of albums specified by the limit parameter.
  5. Only keep the album information from the tuple.