@JoeKun any thoughts on this?
Post
Replies
Boosts
Views
Activity
It turns out you can open Apple Music to the user's library, using /library/ in the path, but it seems that's as deep as it will link.
This works
let urlString = "music://music.apple.com/library"
if let url = URL(string: urlString) {
await UIApplication.shared.open(url)
}
This opens Apple Music to the user's library.
This doesn't
let urlString = "music://music.apple.com/library/playlists"
if let url = URL(string: urlString) {
await UIApplication.shared.open(url)
}
This doesn't
let urlString = "music://music.apple.com/library/playlist/\(playlistID)"
if let url = URL(string: urlString) {
await UIApplication.shared.open(url)
}
The above two open Apple Music, which then displays an error: "An Error Occurred" with a Try Again button
Okay, I tracked this down to an environment variable that was being used by the "dev" (i.e. debug) environment.
As environment variables are not available in "production" (i.e. release), this one particular variable was causing the app to just stop.
There was no message in the console log that reflected this that I could see, though there were guards that threw fatally if the environment variable was not present.
Anyhow, once this was moved to a .plist and called from there, the app loaded as expected in both dev and prod.
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.
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.
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,
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 @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 @JoeKun,
I got distracted for a while, but I wanted to come back to this to provide the following feedback.
After a while of trying various ways to do this, I wound up re-writing the app around Track type instead of Song, as it just seemed to make more sense.
I would not have come up with the below without your feedback to this question.
I really wish the docs would include some code examples, as I still battle finding solutions when reading them in their current written form.
In case it helps anyone, here's what I came up with:
Create a custom type, AppleMusicTrack, that has the properties I want for the app, including track, where I put the Track (MusicKit type) info as well:
struct AppleMusicTrack: Identifiable {
var id: String {
return track.id.rawValue // should probably use MusicItemID here.
}
let track: Track // MusicItemID is in here, when I need it.
let album: String
let addedAt: String
let addedBy: String
let trackType: String
let matchedBy: String
}
Use this function when I can't match a track via isrc
func getAppleMusicTrackBySearch(track: TrackToMatchType) async -> AppleMusicTrack? {
var matchedTrack: AppleMusicTrack? = nil
do {
let albumSearchRequest = MusicCatalogSearchRequest(term: track.track.album.name, types: [Album.self])
let response = try await albumSearchRequest.response()
if(response.albums.count > 0) {
let matchedAlbums = response.albums.filter{album in
track.track.album.artists.contains{$0.name == album.artistName}
}
if matchedAlbums.count > 0 {
let scrapedTracks = await self.getAppleMusicTracksByAlbumId(albums: matchedAlbums)
if(scrapedTracks != nil) {
if let firstMatch = scrapedTracks!.first(
where: {
$0.title == track.track.name
}) {
matchedTrack = AppleMusicTrack(
track: firstMatch,
album: firstMatch.title,
addedAt: track.addedAt,
addedBy: track.addedBy.id,
trackType: "matchedAppleMusicTrack",
matchedBy: "artist/title",
)
}
}
}
}
} catch {
print("Error", error)
// handle error
}
if matchedTrack == nil {
print("No matched track!")
}
return matchedTrack
}
The above calls this function to get tracks by album id:
func getAppleMusicTracksByAlbumId(albums: [Album]) async -> [Track]? {
var scrapedTracks: [Track]?
do {
var albumRequest = MusicCatalogResourceRequest<Album>(matching: \.id, memberOf: albums.map(\.id) )
albumRequest.properties = [.tracks]
let albumResponse = try await albumRequest.response()
let albumsWithTracks = albumResponse.items
let tracks = albumsWithTracks.flatMap { album -> MusicItemCollection<Track> in
album.tracks ?? []
}
scrapedTracks = tracks.count > 1 ? tracks : nil
} catch {
print("Error", error)
}
return scrapedTracks
}
Hi @JoeKun,
You are right again! I would have never guessed [Album.self] by reading the docs, though I do see that in the MusicAlbums app, now that you mention it, thanks. Your patience and support are very much appreciated.
What I still can't find in the docs (nor in the app) is how to do the above albumRequest and include the tracks (songs would be preferable) in the same call.
Unfortunately MusicCatalogSearch request does not have a properties member, so I can't do this:
albumSearchRequest.properties = [.tracks]
At present the above albumResponse only returns the id, title, and artistName.
I've currently worked it out where I do the following:
Get a list of albums based on a search string
use albums.filter{} to reduce this set to only albums by the desired artist, like this:
let matchedAlbums = response.albums.filter{$0.artistName == artistSearchString}
For the record, this actually provides for album info, but not tracks.
Do a MusicCatalogResourceRequest for each album in matchedAlbums to get the tracks
This doable, but tedious for a few reasons, mainly the return is a Track not a Song, and converting these are a pain.
Loop through these to find a track with a criteria matching the one being searched for, which is not hard once the above has been done.
This is excellent info, @JoeKun. Thank you very much for being so super helpful! I gonna need to add you as a co-author for my app 😊
You know, I looked at MusicCatalogResourceRequest before, but the docs confused me. As I am used to working with the Apple Music API, MusicDataRequest seemed more familiar. Thanks for setting me straight.
I'm not sure if this will ever happen, but if the docs had examples like yours in them, they'd be a lot easier to connect the dots.
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
}
}
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"}]
so lame.
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/\(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
}
}`