Hi @talkingsmall,
Thanks much for your thorough and thoughtful reply.
I was unaware (or blanking on) of the .hasNextBatch on MusicItemCollection for some reason. I appreciate you bringing that to my attention, as it helped me come up with a solution (extra logging added for clarity):
@available(iOS 16.0, *)
func getAllTracksFromPlaylistId(id: MusicItemID) async throws -> [Track]? {
func getAllTracks(tracks: MusicItemCollection<Track>) async throws -> [Track]? {
var hasNextBatch = true
var tracksToReturn = tracks.compactMap{$0}
var currentTrackCollection = tracks
Logger.log(.info, "Initial track count: \(tracks.count)")
do {
while hasNextBatch == true {
if let nextBatchCollection = try await currentTrackCollection.nextBatch(limit: 300) { // 300 is max here
tracksToReturn = tracksToReturn + nextBatchCollection.compactMap{$0}
Logger.log(.info, "Fetched \(nextBatchCollection.count) track(s) from next batch")
Logger.log(.info, "Current track subtotal: \(tracksToReturn.count)")
if nextBatchCollection.hasNextBatch {
Logger.log(.info, "nextBatchCollection has nextBatch. Continuing while loop...")
currentTrackCollection = nextBatchCollection
} else {
Logger.log(.info, "nextBatchCollection has no nextBatch. Breaking from while loop")
hasNextBatch = false
}
} else {
Logger.log(.info, "No results from nextBatch()! Breaking from while loop")
hasNextBatch = false
}
}
if tracksToReturn.count > 0 {
Logger.log(.info, "Returning \(tracksToReturn.count) track(s)")
return tracksToReturn
} else {
Logger.log(.info, "tracksToReturn is empty!")
return nil
}
} catch {
Logger.log(.error, "Could not get next batches!")
}
return nil
}
do {
var request = MusicLibraryRequest<MusicKit.Playlist>()
request.filter(matching: \.id, equalTo: id)
let response = try await request.response()
if let playlist = response.items.first {
if let tracks = try await playlist.with(.tracks, preferredSource: .catalog).tracks {
if let allTracks = try await getAllTracks(tracks: tracks) {
Logger.log(.success, "\(playlist.name) has \(allTracks.count) tracks")
return allTracks
} else {
Logger.log(.fire, "Could not fetch any tracks for \(playlist.name)")
}
} else {
Logger.log(.fire, "With tracks on \(playlist.name) returns nil for tracks")
}
} else {
Logger.log(.warning, "Could not find playlist with id: \(id)!")
}
} catch {
Logger.log(.error, "Could not: \(error)")
}
return nil
}
It might be noticed that I've not attempted any concurrency, instead using a while loop to fetch all tracks, serially, in batches.
The reason for this is because, I don't think it speeds things up at all, because I don't think we can't use .nextBatch() in parallel async calls.
In the Apple Music API MusicDataRequest example in my initial post, I can get the total number of tracks in a playlist by looking at the meta.total value and then leverage async calls, using the right offsets to get "next batch" tracks in parallel calls. This significantly improves the speed on playlists with a large number of tracks.
I'm not sure that a MusicItemCollection can provide us with something equivalent to meta.total, and it doesn't appear that .nextBatch() takes an offset argument. So I'm not sure how to speed things up better than what I've come up with here. Of course, it's highly probable that I am mistaken about that.
That said, I can now get all tracks using MusicLibraryRequest to fetch a playlist and using .with on the resultant MusicItemCollection<Playlist>, followed by using .hasNextBatch and .nextBatch(), which is what I was looking for.
Thanks again, @talkingsmall!