How to get the most played Apple Music songs and albums using Swift
Helm Pro yearly subscribers now get a 30% discount on RocketSim thanks to contingent pricing on the App Store.
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
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:
- First, get all the songs in the user’s library using the
song
static method onMPMediaQuery
. - Then, only keep the music items that have been played at least once in the given year.
- Sort the music items from most played to least played.
- Only after the sorting has taken place, keep the number of items specified by the
limit
parameter. - Finally get the full information for the song using
MusicKit
and return the result. Note that I am using theconcurrentMap
method from John Sundell’s CollectionConcurrencyKit to execute all requests concurrently.
Using MusicKit
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:
- First, create a
MusicLibraryRequest
for theSong
type. This type of request does not query Apple Music’s catalog but instead queries the user’s library. - Add a sort descriptor to the request to sort the songs by their
playCount
property in descending order. - Execute the request and get the response.
- Filter the songs to only keep the ones that have been played at least once in the given year.
- 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
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:
- Similarly to the previous example with songs, first get all the albums in the user’s library using the
albums
static method onMPMediaQuery
. - Filter to only keep the music items.
- 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’splaybackStoreID
as the key is that the album media item does not have an id that can be used to fetch the full album information usingMusicKit
. - Filter the albums to only keep the ones that have been played at least once in the given year.
- Sort the albums from most played to least played.
- Only keep the value from the dictionary, which is the song’s
playbackStoreID
. - Only keep the number of albums specified by the
limit
parameter. - Get the full information for the album’s song using
MusicKit
. Note that I am using theconcurrentMap
method from John Sundell’s CollectionConcurrencyKit to execute all requests concurrently. - From the song’s information, get the album information if available and return the result.
Using MusicKit
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:
- First, create a
MusicLibraryRequest
for theAlbum
type. This type of request does not query Apple Music’s catalog but instead queries the user’s library. - 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. - Sort the albums from most played to least played.
- Only keep the number of albums specified by the
limit
parameter. - Only keep the album information from the tuple.