How to get a catalog playlists of a curator?

Hi there,

tl;dr:  How can I get the id of a curator and then with that get a listing of their playlists?

I asked about curators a while back, here in this forum, but I can’t find that post, so I’m writing a new one, esp. now new curator features have been added to MusicKit as of WWDC22.

Let's say that userB wants to access one of userA's playlists.

Since userB has no way of accessing userA’s library, userA’s playlist needs to be accessible as a catalog resource.  This is accomplished by userA manually performing an action in the Music App on macOS or iOS (I wish this could be done programmatically, but no, and that’s off topic).

At present, the only way that I know of to determine if a playlist available as a catalog resource is by checking for the hasCatalog property under attributes and/or the globalId property under attributes.playParams.

@JoeKun once told me that globalId should be considered opaque and should not be relied upon. 

That said, adding "include=catalog" to the request properties of a MusicDataRequest that fetches playlists from userA's Library, like the below will give me access to catalog ID.

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

Note that this can only be performed by userA.

And here comes the curator question:

If userB wants to get a list of all userA’s catalog playlists (or a specific one) how can that be done?

It seems I need to know userA’s curator ID and do some sort of request on that, or I could perhaps get the curator info by performing a MusicCatalogResourceRequest<Playlist> request, adding [.tracks, .moreByCurator] to the request.

On the former, I got no idea how to find userA’s curator ID, even if I’m coding with a scope that has Library access for userA. The best I can seem to do is get a curatorName when adding "include-catalog" (as above) to a MusicDataRequest that gets a user's library playlists.

On the latter, getting the catalog ID of a playlist requires userA to perform a MusicDataRequest against their library and passing that catalogID to userB so that userB can then access the tracks. This is what my app does now.

Ideally though, I'd like to somehow have userB get userA's curator ID and then have userB get userA's catalog playlists without having userA having to do the above.

Any thoughts on how to do this would be most appreciated.

Replies

Trying some more things, admittedly not knowing what I'm doing:

This still requires me to know the catalogId of a playlist that exists as a catalog resource, but it get's me the curatorId, which seems hopeful.

My problem is searching for the curator using a MusicCatalogResourceRequest yields no results.

func findCuratorOfPlaylist(playlistId: String) async throws -> String? {
    do {
        var requestURLComponents = URLComponents()
        requestURLComponents.scheme = "https"
        requestURLComponents.host = "api.music.apple.com"
        requestURLComponents.path = "/v1/catalog/us/playlists/\(playlistId)/curator" // Got this idea from @snuff4
        if let url = requestURLComponents.url {
            let dataRequest = MusicDataRequest(urlRequest: URLRequest(url: url))
            let dataResponse = try await dataRequest.response()
            let decoder = JSONDecoder()
            let response = try decoder.decode(AppleMusicGetCuratorResponse.self, from: dataResponse.data)
            Logger.log(.info, "Curator Response: \(response)") // this returns the id, name, and kind (.editorial)

            let items = response.data
            let id = items[0].id

            Logger.log(.info, "Searching for Curator with ID of \(id)")
            let request = MusicCatalogResourceRequest<Curator>(matching: \.id, equalTo: id)
            let resp = try await request.response()
            Logger.log(.info, "Find Curator Request Response Items: \(resp.items)") // MusicCatalogResourceResponse<Curator>()
            Logger.log(.info, "Is Curator Request Response empty: \(resp.items.isEmpty)") // true
        }
    } catch {
        // handle error
        Logger.log(.error, "Could not findCuratorOfPlaylist \(error)")
    }
    return nil
}

Here's something similar to the above, but using .moreByCurator, though I'm not sure if I'm using it right, as I never get results.

@available(iOS 15.4, *)
func getCuratorPlaylistsFromPlaylist(playlistId: String) async throws -> String? {
    do {
        let request = MusicCatalogResourceRequest<MusicKit.Playlist>(matching: \.id, equalTo: MusicItemID(playlistId)) //catalogId
        let response = try await request.response()
        Logger.log(.success, "Playlists Response: \(response.items)") // this is a collection of playlists, as expected.
        Logger.log(.info, "Playlists Item Count: \(response.items.count)") // this is always 1, as expected
        for item in response.items {
            Logger.log(.info, "Item: \(item)") // shows the playlist's id, name, and curatorName
            Logger.log(.info, "Type of Item: \(type(of: item))") // type is Playlist
            let morePlaylists = item.moreByCurator // not sure this is the right way to use .moreByCurator
            Logger.log(.info, "More Playlists: \(String(describing: morePlaylists))") // This is always nil!
        }
    } catch {
        // handle error
        Logger.log(.error, "Could not findCuratorOfPlaylist \(error)")
    }
    return nil
}

Can't seem to do this either, using the curatorName that can be found in either of the two above attempts.

@available(iOS 15.4, *)
func searchForCurator(name: String) async throws -> MusicItemCollection<Curator>? {
    do {
        Logger.log(.error, "Doing search for curator by name...")
        let request = MusicCatalogSearchRequest(term: name, types: [Curator.self])
        let response = try await request.response()
        Logger.log(.info, "Response curators: \(response.curators)") // MusicItemCollection<Curator>()
        Logger.log(.info, "Response curators isEmpty: \(response.curators.isEmpty)") // true
    } catch {
        // handle error
    Logger.log(.error, "Could not searchForCurator \(error)")
    }
    return nil
}

I'm hopeful, that the **** may work, but I'm waiting on an iOS16 device to test on:

@available(iOS 16.0, *)
func getCuratorPlaylistsFromPlaylist2(playlistId: String) async throws -> String? {
    do {
        var request = MusicCatalogResourceRequest<MusicKit.Playlist>(matching: \.id, equalTo: MusicItemID(playlistId))
        request.properties = [.curator] // adding curator to the request
        let response = try await request.response()
        Logger.log(.success, "Playlists Response: \(response.items)") // this is a collection of playlists, as expected.
        Logger.log(.info, "Playlists Item Count: \(response.items.count)") // this is always 1, as expected
        for item in response.items {
            Logger.log(.info, "Item: \(item)") // shows the playlist's id, name, curatorName, and maybe more with .curator added
            Logger.log(.info, "Type of Item: \(type(of: item))") // type is Playlist
            if let curatorPlaylists = item.curator?.playlists {
                Logger.log(.info, "Curator Playlists: \(curatorPlaylists)") // hopeful here!
            } else {
                Logger.log(.warning, "No Curator Playlists!")
            }
        }
    } catch {
        // handle error
        Logger.log(.error, "Could not findCuratorOfPlaylist \(error)")
    }
    return nil
}