Retrieving a list of tracks from a library playlist

I'm trying to do something that I though would be simple; however, I'm tripping up on something...

In brief, I want to get all tracks from a playlist in an AppleMusic authorized user's playlist with the following function:

    func getTracksFromAppleMusicPlaylist(playlistId: MusicItemID) async throws -> [Track]? {
        print("Fetching AppleMusic Playlists...")
        print("Playlist ID: \(playlistId)")
        var playlistTracks: [Track]? = []
        do {
            var playlistRequest = MusicCatalogResourceRequest<Playlist>(matching: \.id, equalTo: playlistId )
            playlistRequest.properties = [.tracks]
            let playlistResponse = try await playlistRequest.response()
            print("Playlist Response: \(playlistResponse)")
            let playlistWithTracks = playlistResponse.items
            let tracks = playlistWithTracks.flatMap { playlist -> MusicItemCollection<Track> in
                playlist.tracks ?? []
            }
            playlistTracks = tracks.count > 1 ? tracks : nil
        } catch {
            print("Error", error)
            // handle error
        }
        return playlistTracks
    }

This function results in the following error:

2021-08-28 04:25:14.335455+0700 App[90763:6707890] [DataRequesting] Failed to perform MusicDataRequest.Context(
  url: https://api.music.apple.com/v1/catalog/us/playlists/p.7XxxsXxXXxX?include=tracks&omit%5Bresource%5D=autos,
  currentRetryCounts: [.other: 1]
) with MusicDataRequest.Error(
  status: 404,
  code: 40400,
  title: "Resource Not Found",
  detailText: "Resource with requested id was not found",
  id: "QMK7GH4U7ITPMUTTBKIOMXXXX",
  originalResponse: MusicDataResponse(
    data: 159 bytes,
    urlResponse: <NSHTTPURLResponse: 0x00000002820c0b60>
  )
).

The playlistID being used is a value that has been picked from an existing playlist in the user's library, via a function that uses this code snippet:

if let url = URL(string: "https://api.music.apple.com/v1/me/library/playlists?limit=100") {                
   let dataRequest = MusicDataRequest(urlRequest: URLRequest(url: url))
...

The only thing I can think of is that the playlistId from the above snippet is converted to a string when decoding into a struct, after which it is changed back to a MusicItemID with an init, like MusicItemID(playlistId).

Any thoughts? Because I'm at a loss...

Accepted Reply

Hello @Kimfucious,

Indeed, identifiers of tracks or songs from the catalog are in a different space than identifiers of tracks or songs from the library.

In Apple Music API, there are actually different types of objects for catalog and library resources. For example, there are Songs which are used for songs in the catalog, and LibrarySongs.

One thing you could do is to leverage the catalog relationship of Apple Music API's LibrarySongs.

To do that, you can define the following structure:

struct MyLibraryTrack: MusicItem, Codable {
    struct Relationships: Codable {
        let catalog: MusicItemCollection<Track>?
    }
    
    let id: MusicItemID
    let relationships: Relationships?
}

And then use it in conjunction with a MusicDataRequest to load the playlist tracks including the catalog relationship:

var playlistTracksRequestURLComponents = URLComponents()
playlistTracksRequestURLComponents.scheme = "https"
playlistTracksRequestURLComponents.host = "api.music.apple.com"
playlistTracksRequestURLComponents.path = "/v1/me/library/playlists/\(playlistId.rawValue)/tracks"
playlistTracksRequestURLComponents.queryItems = [
    URLQueryItem(name: "include", value: "catalog"), 
]

let playlistTracksRequestURL = playlistTracksRequestURLComponents.url!
let playlistTracksRequest = MusicDataRequest(urlRequest: URLRequest(url: playlistTracksRequestURL))
let playlistTracksResponse = try await playlistTracksRequest.response()

let decoder = JSONDecoder()
let playlistTracks = try decoder.decode(MusicItemCollection<MyLibraryTrack>.self, from: playlistTracksResponse.data)

print("Playlist with ID \(playlistId) has tracks:")
for (i, track) in playlistTracks.enumerated() {
    if let correspondingCatalogTrack = track.relationships?.catalog?.first {
        print("    \(i) => \(track.id) corresponds to catalog track with ID: \(correspondingCatalogTrack.id).")
    }
    else {
        print("    \(i) => \(track.id) doesn't have any corresponding catalog track.")
    }
}

When testing this code with a sample playlist, I see the following:

Playlist with ID p.pkgS2BZZpN has tracks:
    0 => i.xVexf4lLLXO corresponds to catalog song with ID: 320697087.
    1 => i.dZext6kddXM doesn't have any corresponding catalog song.
    2 => i.vv59U9P6641 corresponds to catalog song with ID: 632895520.
    3 => i.rxJZfD2001x corresponds to catalog song with ID: 592365007.
    4 => i.dlJzf6kddXM corresponds to catalog song with ID: 697195787.
    5 => i.79eOiVJPPZW corresponds to catalog song with ID: 1190935915.
    6 => i.E9rMIzOYYPl doesn't have any corresponding catalog song.
    7 => i.B9paue544zP corresponds to catalog song with ID: 327818744.
    8 => i.0qW2HV2773X corresponds to catalog song with ID: 736211119.
    9 => i.dOBxS6kddXM doesn't have any corresponding catalog song.
    10 => i.NaYmCP0VVdO doesn't have any corresponding catalog song.
    11 => i.9algtNZ88R3 corresponds to catalog song with ID: 1451768057.
    12 => i.99mmSNZ88R3 corresponds to catalog song with ID: 1140562212.

I believe this will unblock you to implement the logic you've been trying to implement lately.

I hope this helps.

Best regards,

Replies

Hello @Kimfucious,

Thanks for your question about loading tracks from a playlist.

The correct way to load the tracks relationship of a Playlist is to use the with(_:) method passing in the tracks property:

let playlist: Playlist = …
let detailedPlaylist = try await playlist.with([.tracks])
if let tracks = detailedPlaylist.tracks {
    print("Playlist tracks: \(tracks)")
}

The same pattern applies to all relationships and associations in MusicKit, as explained at the beginning of our WWDC session Meet MusicKit for Swift.

I hope this helps.

Best regards,

Hi @JoeKun,

Thanks for the quick reply.

I was using the pattern from here, which seemed appropriate, but again, I get lost in the docs, as there are no examples there.

In your above response, you've put an ellipsis in, which would be helpful to know what's there. Is that supposed to be?:

let playlistRequest = MusicCatalogResourceRequest<Playlist>(matching: \.id, equalTo: playlistId )

Shouldn't there be a .response() here?:

let detailedPlaylist = try await playlist.with([.tracks])

Please provide a full example of the code, if you can, time permitting.

For the record, I did see this code in the video you mentioned, but I've been using MusicCatalogRequest for so many things, it didn't click until you just said it.

Hi @Kimfucious,

No, this snippet of code doesn't require creating any request, nor calling .response(). It's pretty much exactly as I showed it.

The line with an ellipsis clearly showed what the variable was supposed to be:

let playlist: Playlist = …

It's supposed to be an instance of Playlist.

I was assuming that, since you had a playlistId, you must have loaded a playlist. Isn't that right? Where were you getting the argument you were passing to your function getTracksFromAppleMusicPlaylist?

Best regards,

Hi @JoeKun,

All I have at the time of making the above function call is the id of the playlist.

So at this time there is no instance of a playlist, unless I'm mistaken.

To provide more insight as to what I'm doing:

  • this function is run prior to adding tracks to an existing playlist
  • it should get all of the tracks in the playlist, so that duplicate tracks are not added.
  • the playlistId that is passed into the function is the id (MusicItemID) of the playlist.

Not sure how I get from this point to where I have an instance of the playlist.

Hi @Kimfucious,

Ok, interesting. So how are you even getting this playlistId in the first place?

  • func loadLibraryPlaylists() async throws { let request = MusicLibraryRequest<Playlist>() let response = try await request.response()

    resetPlaylist() for item in response.items { playlists.append(ApplePlaylistModel(name: item.name, playlistId: item.id.rawValue)) } }
Add a Comment

The playlistID is originally retrieved from the following function:

    func getCurrentUsersAppleMusicPlaylists() async throws -> [AppleMusicPlaylist] {
        print("Fetching AppleMusic Playlists...")
        var playlists: [AppleMusicPlaylist] = []
        do {
            if let url = URL(string: "https://api.music.apple.com/v1/me/library/playlists?limit=100") {
                let dataRequest = MusicDataRequest(urlRequest: URLRequest(url: url))
                let dataResponse = try await dataRequest.response()
                let decoder = JSONDecoder()
                decoder.keyDecodingStrategy = .convertFromSnakeCase
                let response = try decoder.decode(AppleMusicPlaylistsResponse.self, from: dataResponse.data)
                playlists = response.data
                print("Playlists Fetched")
            }
        } catch {
            print("Error Fetching Current User's Playlists", error)
            throw error
        }
        return playlists
    }

These are stored as a @Published value in an ObservableObject.

Later on in the app, the user selects a default playlist, using a picker, to save tracks to, and that is saved in a @Published variable in an ObservableObject (passed as an environment variable from higher up the chain), which is of type:

struct DefaultSaveToPlaylist: Codable, Hashable {
    let id: String
    let name: String
    let type: String
}

When getCurrentUsersAppleMusicPlaylists()is called, the id (type String) above is converted to an MusicItemID, via MusicItemId(playlistId) and passed as a parameter to the function call.

The reasoning behind that final separation is because the detaulSaveToPlaylist is stored in a cloud database and shared across other instances of the app running different devices and operating systems.

Hi @JoeKun,

One thing I noticed in the error message is that the url is:

https://api.music.apple.com/v1/catalog/us/playlists/p.7J5xs48VxVx?include=tracks&omit%5Bresource%5D=autos

This url is missing the me/library, using instead catalog, which leads me to believe that MusicCatalogResourceRequest is not appropriate for fetching items in a user's library, and that this is where I would need to use a MusicDataRequest.

Hello @Kimfucious,

Thanks for this additional feedback.

You are entirely correct: MusicCatalogResourceRequest is not designed to load library resources. It only supports loading resources from the Apple Music catalog.

I would definitely recommend that you consider including the entire Playlist in your DefaultSaveToPlaylist structure.

Doing so is very easy because it already conforms to Codable. Furthermore, the encoded payload for the playlist corresponds exactly to the JSON representation of a playlist resource coming straight from Apple Music API. So, with this kind of design, you would still be able to support other platforms, including non-Apple platforms.

That said, if you ever needed to get the list of tracks from a library playlist based solely on its identifier, you could do it using MusicDataRequest like this:

var playlistRequestURLComponents = URLComponents()
playlistRequestURLComponents.scheme = "https"
playlistRequestURLComponents.host = "api.music.apple.com"
playlistRequestURLComponents.path = "/v1/me/library/playlists/\(playlistId.rawValue)"
playlistRequestURLComponents.queryItems = [
    URLQueryItem(name: "include", value: "tracks"), 
]

if let playlistRequestURL = playlistRequestURLComponents.url {
    let playlistRequest = MusicDataRequest(urlRequest: URLRequest(url: playlistRequestURL))
    let playlistResponse = try await playlistRequest.response()
    
    let decoder = JSONDecoder()
    let playlists = try decoder.decode(MusicItemCollection<Playlist>.self, from: playlistResponse.data)
    if let playlist = playlists.first {
        let playlistTracks = playlist.tracks ?? []
        print("\(playlist) has tracks:\n\(playlistTracks)")
    }
}

Please note how this snippet of code uses MusicItemCollection for the type passed into JSONDecoder's decode method. It's much easier and less code than creating a custom data structure. Additionally, in other scenarios like in your getCurrentUsersAppleMusicPlaylists function, using MusicItemCollection in this manner allows you to very easily get access to the next batch of playlists, in case the user has more than 100 of them.

I hope this helps.

Best regards,

Thanks again, @JoeKun!

I was able to retrieve the tracks using your code example above. Your mention of URLComponents saved me a lot of time, as appending the a query param wasn't working as I thought it would.

That said, I'm a befuddled, as there seems to be a mismatch between track ids when retrieved via the above code example you provided, and those retrieved using MusicCatalogRequest and/or MusicCatalogSearchRequest.

Again, what I'm trying to do is prevent the addition of tracks that already exist in a user's playlist, so I'm checking the playlist contents prior to adding the tracks.

To do this: I create an array (Array 1) of ids from the tracks fetched in the above function (via MusicDataRequest), then I filter over an array (Array 2) of tracks that have been fetched via MusicCatalogRequest or MusicCatalogSearchRequest.

In each filter loop, I only return if the Array 2 element's id is not in Array 1.

Something like this:

let tracksNotInPlaylist = Array2.filter{!array1.contains($0.id)}

Then I only add the tracks that are in the tracksNotInPlaylist array to the playlist, preventing the undesired duplication.

This isn't working, so I logged and compared the values of the ids.

Apparently, the track IDs in Array 1 are strings, in the form of: i.RB4aoUZ5EAmd, and the ids of the tracks in Array2 are integers, such as: 1023383902.

My assumption is that MusicItemCollection<Track> stores IDs differently than items of type Track.

I'm at a loss of how to get these to align.

Hello @Kimfucious,

Indeed, identifiers of tracks or songs from the catalog are in a different space than identifiers of tracks or songs from the library.

In Apple Music API, there are actually different types of objects for catalog and library resources. For example, there are Songs which are used for songs in the catalog, and LibrarySongs.

One thing you could do is to leverage the catalog relationship of Apple Music API's LibrarySongs.

To do that, you can define the following structure:

struct MyLibraryTrack: MusicItem, Codable {
    struct Relationships: Codable {
        let catalog: MusicItemCollection<Track>?
    }
    
    let id: MusicItemID
    let relationships: Relationships?
}

And then use it in conjunction with a MusicDataRequest to load the playlist tracks including the catalog relationship:

var playlistTracksRequestURLComponents = URLComponents()
playlistTracksRequestURLComponents.scheme = "https"
playlistTracksRequestURLComponents.host = "api.music.apple.com"
playlistTracksRequestURLComponents.path = "/v1/me/library/playlists/\(playlistId.rawValue)/tracks"
playlistTracksRequestURLComponents.queryItems = [
    URLQueryItem(name: "include", value: "catalog"), 
]

let playlistTracksRequestURL = playlistTracksRequestURLComponents.url!
let playlistTracksRequest = MusicDataRequest(urlRequest: URLRequest(url: playlistTracksRequestURL))
let playlistTracksResponse = try await playlistTracksRequest.response()

let decoder = JSONDecoder()
let playlistTracks = try decoder.decode(MusicItemCollection<MyLibraryTrack>.self, from: playlistTracksResponse.data)

print("Playlist with ID \(playlistId) has tracks:")
for (i, track) in playlistTracks.enumerated() {
    if let correspondingCatalogTrack = track.relationships?.catalog?.first {
        print("    \(i) => \(track.id) corresponds to catalog track with ID: \(correspondingCatalogTrack.id).")
    }
    else {
        print("    \(i) => \(track.id) doesn't have any corresponding catalog track.")
    }
}

When testing this code with a sample playlist, I see the following:

Playlist with ID p.pkgS2BZZpN has tracks:
    0 => i.xVexf4lLLXO corresponds to catalog song with ID: 320697087.
    1 => i.dZext6kddXM doesn't have any corresponding catalog song.
    2 => i.vv59U9P6641 corresponds to catalog song with ID: 632895520.
    3 => i.rxJZfD2001x corresponds to catalog song with ID: 592365007.
    4 => i.dlJzf6kddXM corresponds to catalog song with ID: 697195787.
    5 => i.79eOiVJPPZW corresponds to catalog song with ID: 1190935915.
    6 => i.E9rMIzOYYPl doesn't have any corresponding catalog song.
    7 => i.B9paue544zP corresponds to catalog song with ID: 327818744.
    8 => i.0qW2HV2773X corresponds to catalog song with ID: 736211119.
    9 => i.dOBxS6kddXM doesn't have any corresponding catalog song.
    10 => i.NaYmCP0VVdO doesn't have any corresponding catalog song.
    11 => i.9algtNZ88R3 corresponds to catalog song with ID: 1451768057.
    12 => i.99mmSNZ88R3 corresponds to catalog song with ID: 1140562212.

I believe this will unblock you to implement the logic you've been trying to implement lately.

I hope this helps.

Best regards,