Adding tracks to User's Library Playlist

Hi there,

I'm creating an iOS 15 app, using the new Swift MusicKit beta.

I've got basic "arbitrary" GET calls working with MusicDataRequest, like: fetch playlists and tracks.

However, I cannot find good docs for adding tracks to a playlist in a user's library.

I guess I could do this the "old way", but then I don't get the super-nice feature of auto dev/user tokens.

So the question is: how to a POST, instead of GET in the following:

let url = URL(string: "https://api.music.apple.com/v1/me/library/playlists/`\(targetPlaylistId)/tracks")
let dataRequest = MusicDataRequest(urlRequest: URLRequest(url: url)) 
Answered by Frameworks Engineer in 682412022

Hi @Kimfucious,

You are trying to use the Add Tracks to a Library Playlist endpoint. If you look at that documentation carefully, you'll see that the body is expected to be a dictionary containing a data member that points to an array much like the one you're already creating.

So I believe all you're missing is a little structure like:

  struct AppleMusicPlaylistPostRequestBody: Codable {
    let data: [AppleMusicPlaylistPostRequestItem]
  }

Then you can adjust your code as follows:

  let requestBody = AppleMusicPlaylistPostRequestBody(
    data: tracksToAdd.compactMap {
      AppleMusicPlaylistPostRequestItem(id: $0.id, type: "songs")
    }
  }
  
  […]
  
      urlRequest.httpBody = try encoder.encode(requestBody)

I hope this helps.

Best regards,

I've made some progress here and am drawing some assumptions, which I hope to get verified in this thread.

Assumption #1: MusicDataRequest() doesn't support POST requests. I tried many different ways to do this and kept getting parsing errors when trying to pass an encoded data object in the body.

So I gave up on using MusicDataRequest and went "old school" with the below.

Assumption #2: As the below code results in a 400 (bad request) status and there is no additional information in the response, I assume there is something wrong with the request, most likely a bad request.httpBody entry.

The below is, as best I can code in Swift right now, the same call I make in a JS app.

`func addTracksToAppleMusicPlaylist(targetPlaylistId: String, tracksToAdd: [AppleMusicTracks]) async throws -> Void {   let tracks = tracksToAdd.compactMap{     AppleMusicPlaylistPostRequest(id: $0.id, type: "songs")   }   do {     print("Saving tracks to Apple Music Playlist: (targetPlaylistId)")     let tokens = try await self.getAppleMusicTokens() // <-this works correctly

    if let url = URL(string: "https://api.music.apple.com/v1/me/library/playlists/%5C(targetPlaylistId)/tracks") {       var request = URLRequest(url: url)       request.httpMethod = "POST"       request.addValue("Bearer (tokens.devToken)", forHTTPHeaderField: "Authorization")       request.addValue("(tokens.userToken)", forHTTPHeaderField: "Music-User-Token")       request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")       let encoder = JSONEncoder()       let data = try encoder.encode(tracks)       request.httpBody = data       let session = URLSession(configuration: .default)       let task = session.dataTask(with: request) {(data, response, error) in         if error == nil {           if let httpResponse = response as? HTTPURLResponse {             print("statusCode: (httpResponse.statusCode)")             print("response: (httpResponse)"). // <= 400 response here           }         } else {           print("Error saving tracks to playlist (String(describing: error))")         }       }       task.resume()     } else {       print("Bad URL!")     }   } catch {     print("Error Saving Tracks to Playlist", error)     throw error   } }`

so lame.

Hello @Kimfucious,

Thank you for your question.

You shouldn't have to do all this work manually with URLSession.

You should be able to use MusicDataRequest so it can do all the heavy lifting for you with respect to decorating the request with the relevant tokens for accessing Apple Music API.

Can you try something like this instead?

let url = URL(string: "https://api.music.apple.com/v1/me/library/playlists/\(targetPlaylistId)/tracks")!
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
let dataRequest = MusicDataRequest(urlRequest: urlRequest) 

I believe this should work.

I hope this helps.

Best regards,

Hi @JoeKun, thanks for the response.

I did try something similar to the above with MusicDataRequest ; however, as I'm doing a POST, I needed to also send a body. And I was getting errors, which had me thinking that the problem was potentially with MusicDataRequest.

Any further help to get this working, would be much appreciated.

The below code throws the following error:

MusicDataRequest.Error(
 status: 400,
 code: 40007,
 title: "Invalid Request Body",
 detailText: "Unable to parse request body",
 id: "CKYZ347O6YJDXVPVSTZJOQ4R6Y",
 originalResponse: MusicDataResponse(
  data: 149 bytes,
  urlResponse: <NSHTTPURLResponse: 0x0000000283e9ed00>
 )
)

Please see comments within the below:

func addTracksToAppleMusicPlaylist(targetPlaylistId: String, tracksToAdd: [Song]) async throws -> Void {
  
  struct AppleMusicPlaylistPostRequestItem: Codable {
    let id: MusicItemID
    let type: String
  }

  // This is what I have done using when updating a playlist in a JS app (but without the type)
  // My guess is that the problem is with the encoded data

  let tracks = tracksToAdd.compactMap{
    AppleMusicPlaylistPostRequestItem(id: $0.id, type: "songs")
  }

  do {
    print("Saving matched tracks to Apple Music Playlist: \(targetPlaylistId)")
    if let url = URL(string: "https://api.music.apple.com/v1/me/library/playlists/\(targetPlaylistId)/tracks") {
      var urlRequest = URLRequest(url: url)
      urlRequest.httpMethod = "POST"
      let encoder = JSONEncoder()
      let data = try encoder.encode(tracks)
      print(String(data: data, encoding: .utf8)!) // <== See result below.
      urlRequest.httpBody = data // <== This is most likey the problem!
      let musicRequest = MusicDataRequest(urlRequest: urlRequest)
      let musicRequestResponse = try await musicRequest.response()
      // maybe do something with response once I get this working...
    } else {
      print("Bad URL!")
      throw AddToPlaylistError.badUrl(message: "Bad URL!")
    }
  } catch {
    print("Error Saving Tracks to Playlist", error)
    throw error
  }
}

The output from that print command, above, looks like this, which seems right:

[{"id":"426656373","type":"songs"},{"id":"1559839880","type":"songs"},{"id":"1498420189","type":"songs"}]
Accepted Answer

Hi @Kimfucious,

You are trying to use the Add Tracks to a Library Playlist endpoint. If you look at that documentation carefully, you'll see that the body is expected to be a dictionary containing a data member that points to an array much like the one you're already creating.

So I believe all you're missing is a little structure like:

  struct AppleMusicPlaylistPostRequestBody: Codable {
    let data: [AppleMusicPlaylistPostRequestItem]
  }

Then you can adjust your code as follows:

  let requestBody = AppleMusicPlaylistPostRequestBody(
    data: tracksToAdd.compactMap {
      AppleMusicPlaylistPostRequestItem(id: $0.id, type: "songs")
    }
  }
  
  […]
  
      urlRequest.httpBody = try encoder.encode(requestBody)

I hope this helps.

Best regards,

Hi @JoeKun,

Your reply was spot on! As you can probably tell, I'm new to Swift. So thanks for your patience!

In JS, I rely on Axios for https requests, and it handles the "data" somewhat automagically, so I completely glossed over it here.

I want to thank you for your response and helping me get this resolved. Frankly, I've never had too much luck getting quality answers from this forum, and now that's changed, which is great!

Below is my final code, in case it helps someone.

struct AppleMusicPlaylistPostRequestBody: Codable {
    let data: [AppleMusicPlaylistPostRequestItem]
}

struct AppleMusicPlaylistPostRequestItem: Codable {
    let id: MusicItemID
    let type: String
}

func addTracksToAppleMusicPlaylist(targetPlaylistId: String, tracksToAdd: [Song]) async throws -> Void {

    let tracks = AppleMusicPlaylistPostRequestBody(data: tracksToAdd.compactMap {
        AppleMusicPlaylistPostRequestItem(id: $0.id, type: "songs")
    })

    do {
        if let url = URL(string: "https://api.music.apple.com/v1/me/library/playlists/\(targetPlaylistId)/tracks") {
            var urlRequest = URLRequest(url: url)
            urlRequest.httpMethod = "POST"
            let encoder = JSONEncoder()
            let data = try encoder.encode(tracks)
            urlRequest.httpBody = data
            let musicRequest = MusicDataRequest(urlRequest: urlRequest)
            let musicRequestResponse = try await musicRequest.response()
            print("Music Request Response", musicRequestResponse)
            // Notify user of success!
        } else {
            print("Bad URL!")
            throw AddTracksToPlaylistError.badUrl(message: "Bad URL!")
        }
    } catch {
        print("Error Saving Tracks to Playlist", error)
        throw error
    }
}

Hello @Kimfucious,

I just wanted to let you know you no longer need to use MusicDataRequest on iOS 16 beta 1 to add tracks to a playlist from the user's library.

Instead, you can now achieve the same behavior with new support added this year for accessing the user's library with MusicKit.

Specifically, you should look into MusicLibrary's new method to add items to a playlist.

Please check our new WWDC22 session video, Explore more content with MusicKit, which goes into this, and much more!

I hope you'll like it!

Best regards,

Thanks for the pro follow-up, @JoeKun.

I've been dabbling, and it looks like add(to:) is singular, while edit would allow me to achieve my use case.

Nice to see these new features coming to MusicKit!

Adding tracks to User's Library Playlist
 
 
Q