Hi there,
tl;dr: What's the best way to get all tracks (with catalog IDs) from a playlist that has more than 100 tracks, using MusicLibraryRequest.
I'm doing something dumb, not understanding something, and possibly both.
I've got an existing, kinda okay function that uses the MusicDataRequest and the Apple Music API to fetch all tracks from a playlist, with pagination like this:
func getTracksFromAppleMusicLibraryPlaylist(playlist: AppleMusicPlaylist) async throws -> [MusicKit.Track]? {
var tracksToReturn: [MusicKit.Track] = []
var libraryTracks: [AppleMusicLibraryTrack] = []
@Sendable
func fetchTracks(playlist: AppleMusicPlaylist, offset: Int) async -> AppleMusicPlaylistFetchResponse? {
do {
let playlistId = playlist.id
var playlistRequestURLComponents = URLComponents()
playlistRequestURLComponents.scheme = "https"
playlistRequestURLComponents.host = "api.music.apple.com"
playlistRequestURLComponents.path = "/v1/me/library/playlists/\(playlistId)/tracks"
playlistRequestURLComponents.queryItems = [
URLQueryItem(name: "include", value: "catalog"),
URLQueryItem(name: "limit", value: "100"),
URLQueryItem(name: "offset", value: String(offset)),
]
if let playlistRequestURL = playlistRequestURLComponents.url {
let playlistRequest = MusicDataRequest(urlRequest: URLRequest(url: playlistRequestURL))
let playlistResponse = try await playlistRequest.response()
let decoder = JSONDecoder()
// print("Get Tracks Dump")
// print(String(data: playlistResponse.data, encoding: .utf8)!)
let response = try decoder.decode(AppleMusicPlaylistFetchResponse.self, from: playlistResponse.data)
return response
} else {
print("Bad URL!")
}
} catch {
print(error)
}
return nil
}
Logger.log(.info, "Fetching inital tracks from \(playlist.attributes.name)")
if let response = await fetchTracks(playlist: playlist, offset: 0) {
if let items = response.data {
libraryTracks = items
}
if let totalItemCount = response.meta?.total {
Logger.log(.info, "There are \(totalItemCount) track(s) in \(playlist.attributes.name)")
if totalItemCount > 100 {
let remainingItems = (totalItemCount - 100)
let calls = remainingItems <= 100 ? 1 : (totalItemCount - 100) / 100
Logger.log(.info, "Total items: \(totalItemCount)")
Logger.log(.info, "Remaining items: \(remainingItems)")
Logger.log(.info, "Calls: \(calls)")
await withTaskGroup(of: [AppleMusicLibraryTrack]?.self) { group in
for offset in stride(from: 100, to: calls * 100, by: 100) {
Logger.log(.info, "Fetching additional tracks from \(playlist.attributes.name) with offset of \(offset)")
group.addTask {
if let response = await fetchTracks(playlist: playlist, offset: offset) {
if let items = response.data {
return items
}
}
return nil
}
}
for await (fetchedTracks) in group {
if let tracks = fetchedTracks {
libraryTracks.append(contentsOf: tracks)
}
}
}
}
}
}
// props to @JoeKun for this bit of magic
Logger.log(.info, "Matching library playlist tracks with catalog tracks...")
for (i, track) in libraryTracks.enumerated() {
if let correspondingCatalogTrack = track.relationships?.catalog?.first {
tracksToReturn.append(correspondingCatalogTrack)
print("\(i) => \(track.id) corresponds to catalog track with ID: \(correspondingCatalogTrack.id).")
} else {
Logger.log(.warning, "\(i) => \(track.id) doesn't have any corresponding catalog track.")
}
}
if tracksToReturn.count == 0 {
return nil
}
return tracksToReturn
}
While not the most elegant, it gets the job done, and it's kinda quick due to the use of withTaskGroup .esp with playlists containing more than 100 songs/tracks.
Regardless, I'm kinda stuck, trying to do something similar with the new MusicLibraryReqeust in iOS 16.
The only way I can think of to get tracks from a playlist, using MusicLibraryRequest, having read the new docs, is like this:
@available(iOS 16.0, *)
func getAllTracksFromHugePlaylist(id: MusicItemID) async throws -> [MusicKit.Track]? {
do {
var request = MusicLibraryRequest<MusicKit.Playlist>()
request.filter(matching: \.id, equalTo: id)
let response = try await request.response()
if response.items.count > 0 {
if let tracks = try await response.items[0].with(.tracks, preferredSource: .catalog).tracks {
Logger.log(.info, "Playlist track count: \(tracks.count)")
return tracks.compactMap{$0}
}
}
} catch {
Logger.log(.error, "Could not: \(error)")
}
return nil
}
The problem with this is that .with seems to be capped at 100 songs/tracks, and I can't see any way to change that.
Knowing that, I can't seem to tell MusicLibraryRequest that I want the tracks of the playlist with the initial request, where before I could use request.properties = .tracks, which I could then paginate if available.
Any help setting me on the right course would be greatly appreciated.
I've been trying to get better at structured concurrency, so I took a stab at a function that collects all of the tracks from a library playlist. I think the key here is using the hasNextBatch property on MusicItemCollection. I'm far from on expert in this world, so I'm sure this code can be improved. In the meantime, here's what I came up with. It correctly returns the number of songs on playlists with more than 100 songs.
func getTracksFromPlaylist(name: String) async throws {
var request = MusicLibraryRequest<Playlist>.init()
request.filter(text: name)
let result = try! await request.response()
if let first = result.items.first {
let withTracks = try await first.with(.tracks)
guard let startingTracks = withTracks.tracks else {
return
}
try await getAllTracksFromPlaylist(startingTracks: startingTracks, group: nil)
}
}
func getAllTracksFromPlaylist(startingTracks: MusicItemCollection<Track>, group: ThrowingTaskGroup<[Track],Error>?) async throws {
//If the group didn't come in as nil, that means startingTracks has a next batch that needs to be fetched and processed
if var group = group {
if let currentSetToProcess = try await startingTracks.nextBatch() {
//Add the tracks from this batch to the group
group.addTask {
return Array(currentSetToProcess)
}
//If this set has a next batch, run the function again. Once currentSetToProcess.hasNextBatch returns false, the group will complete since it is not awaiting anything else.
if currentSetToProcess.hasNextBatch {
try await getAllTracksFromPlaylist(startingTracks: currentSetToProcess, group: group)
}
}
//If the group came in as nil, create the group and work through the first batch of tracks.
} else {
try await withThrowingTaskGroup(of: [Track].self, body: { group in
//The array to store the final set of tracks
var allTracks: [Track] = []
//Add the starting tracks to the group
group.addTask {
return Array(startingTracks)
}
//If there are more than 100 tracks on the playlist, this is true and means we need to loop through and get the other tracks.
if startingTracks.hasNextBatch {
try await getAllTracksFromPlaylist(startingTracks: startingTracks, group: group)
}
//As arrays of tracks are added to the group, append them to our allTracks array.
for try await tracks in group {
allTracks.append(contentsOf: tracks)
}
//Finally, when the group has returned all of the tracks, print the final amount.
print(allTracks.count)
})
}
}