Thoughts on MusicLibraryRequest as a replacement for MPMediaQuery

I'm very excited about the new MusicLibrary API, but after a couple of days of playing around with it, I have to say that I find the implementation of filtering MusicLibraryRequests a little confusing. MPMediaQuery has a fairly extensive list of predicates that can be applied, including string and persistentID comparisons for artist, album artist genre, and more. It also lets you filter on an item’s title. MusicLibraryRequests let you filter on the item’s ID, or on its MusicKit Artist and Genre relationships. To me, this seems like it adds an extra step. 

With an MPMediaQuery, if I wanted to fetch every album by a given artist, I’d apply an MPMediaPropertyPredicate looking at MPMediaItemPropertyAlbumArtist and compare the string. It was also easy to change the MPMediaPredicateComparison to .contains to match more widely. If I wanted to surface albums by “Aesop Rock” or “Aesop Rock & Blockhead,” I could use that.

In the MusicLibraryRequest implementation, it looks like I need to perform a MusicLibraryRequest<Artist> first in order to get the Artist objects. There’s no filter for the name property, so if I don’t have their IDs, I’ve got to use filter(text:). From there, I can take the results of that request and apply them to my MusicLibraryRequest<Album> using the filter(matching:memberOf) function. 

I could use filter(text:) on the MusicLibraryRequest<Album>, but that filters across multiple properties (title and artistName?) and is less precise than defining the actual property I want to match against.

I think my ideal version of the MusicLibraryRequest API would offer something like filter(matching:equalTo:) or filter(matching:contains:) that worked off of KeyPaths rather than relationships. That seems more intuitive to me. I’m not saying we need every property from every filterable MPMediaItemProperty key, but I’d love to be able to do it on title, artistName, and other common metadata. That might look something like:

filter(matching: \.title, contains: “Abbey Road”)
filter(matching: \.artistName, equalTo: “Between The Buried And Me”)

I noticed that filter(text:) is case insensitive, which is awesome, and something I’ve wanted for a long time in MPMediaPropertyPredicate. As a bonus, it would be great if a KeyPath based filter API supported a case sensitivity flag. This is less of a problem when dealing with Apple Music catalog content, but users’ libraries are a harsh environment, and you might have an artist “Between The Buried And Me” and one called “Between the Buried and Me.” It would be great to get albums from both with something like:

filter(matching: \.artistName, equalTo: “Between The Buried And Me”, caseSensitive: false) 

I've submitted the above as FB10185685. I also submitted another feedback this morning regarding filter(text:) and repeating text as FB10184823.

My last wishlist item for this API (for the time being!) is exposing the MPMediaItemPropertyAlbumPersistentID as an available filter attribute. I know, I know… hear me out. If you take a look at the other thread I made today, you’ll see that due to missing metadata in MusicKit, I still have some use cases where I need to be able to reference an MPMediaItem and might need to fetch its containing MPMediaItemCollection to get at other tracks on the album. It would be nice to seamlessly be able to fetch the MPMediaItemCollection or the library Album using a shared identifier, especially when it comes to being able to play the album in MusicKit’s player rather than Media Player’s. 

I've submitted that list bit as FB10185789

Thanks for bearing with my walls of text today. Keep up the great work!

Post not yet marked as solved Up vote post of talkingsmall Down vote post of talkingsmall
2.4k views

Replies

Hello @talkingsmall,

Thanks again for providing great feedback early in the beta period of iOS 16.

To talk about a specific example which isn't so different from the ones you used, while I understand that you might like to find songs of a certain album by requesting songs directly, filtering on the albumTitle, this approach may not be as robust as narrowing it down to one specific album. Indeed, it's quite possible that one might have two completely different albums, of different artists, with the exact same title. Hence, narrowing the results based on a specific item, as opposed to a string, may in fact lead to a better behavior.

Furthermore, we already use key-paths for our various library filters. It's just that we explicitly bless the key-paths of properties we know you can filter by, and we do so by including them into specific library filter protocols (here is the one for Album, as an example). Doing that has many advantages, because you know at compile time what you can actually filter by (so you're never surprised by any kind of fatal error at runtime in case one property wasn't actually filterable), and therefore it allows for very nice autocompletion in Xcode for the key-path argument next to the matching: label.

Nevertheless, for an API like MusicLibraryRequest, I think it's fair to say that we should look into providing more options for filtering based on specific strings.

We'll make sure to investigate these various tickets as well.

Best regards,

Thanks, @JoeKun! My bad -- yes, I suppose just because they aren't the key paths I want doesn't mean they aren't key paths to begin with ;)

I guess the proper request is something like: please add the following properties to the following protocols to enable more specific filtering in MusicLibraryRequests:

LibrarySongFilter

albumTitle
artistName
composerName
title

LibraryAlbumFilter

artistName
title

LibraryArtistFilter

name

Hi @talkingsmall,

Yep, that is my biggest takeaway from your feedback, that we need to look into adding those specific properties to the various library filter protocols.

We'll keep investigating.

Best regards,

Hello @talkingsmall,

We made a number of important improvements to the list of available properties you can filter with and sort by using MusicLibraryRequest. You can find those in iOS 16 Beta 3, which just we just released today.

For example, the list of available library filters for Song now includes:

Additionally, the list of properties you can sort library songs by now includes new interesting properties like:

Not only can you now sort results by those properties, but they're also available as properties of the relevant music item structures (for example, Song now exposes playCount).

Moreover, we made another change that allows you to pass the value for MPMediaItemPropertyAlbumPersistentID to filter methods from MusicLibraryRequest with the \.id key-path.

I hope you will find the capabilities of MusicLibraryRequest to be more well rounded in this latest Beta, and that you'll be able to adopt it in your app for all your music library needs.

Best regards,

  • Is there a way to do this with the Apple Music API or Music Kit JS?

Add a Comment

@JoeKun this is amazing! I'm so grateful for all of these improvements. Having the ability to return MusicKit Album objects from an MPMediaItemPropertyAlbumPersistentID makes it so much simpler for me to adopt all this awesome new stuff. I have noticed a couple of issues, for which I've filed feedbacks:

FB10581774 - Non-alphanumeric/whitespace characters don't work in equalTo matching

When testing the new LibraryAlbumFilter properties, I think I've found an issue where matching on \.artistName or \.title fails to return results when using the equalTo match type and matching on a string containing non-alphanumeric or whitespace characters. The below code fails to return any results when using equalTo, but using the contains version of the filter function correctly returns all the R.E.M. albums in my library. I have also reproduced this same issue when matching on \.title when the album name contains parentheses.


  var nameRequest = MusicLibraryRequest<Album>.init()
        nameRequest.filter(matching: \.artistName, equalTo: "R.E.M.")
        
        do {
            
            let nameResponse = try await nameRequest.response()
                        
            
            print(nameResponse.items)

 } catch {
         
            print("name request error: \(error)")

        }

FB10581868 - Some MPMediaItemPropertyAlbumPersistentID matches fail

Using the below code successfully returns MusicKit albums in most cases, but sometimes it fails to. Here's my code for the match starting with an MPMediaItem called rep.

      let idString = String(rep.albumPersistentID)
          var request = MusicLibraryRequest<Album>()
        
        request.filter(matching: \.id, equalTo: MusicItemID(idString))
        
        do {
            
            let response = try await request.response()
            print(response.items)

        } catch {
            print("id request error: \(error)")
           
        }
        

In cases where matching on this ID doesn’t work, I then do a backup search by .artistName and .title, which returns the album and shows a different ID.

The ones that fail show an ID with a huge negative number. It seems like the original MPMediaItemPropertyAlbumPersistentID is unsigned, but the ID MusicKit is using is? Something like that?

Here's what prints when I print the ID:

Looking for Defeater - Abandoned (Deluxe Edition), id: 9601744824086363441 

Here's what prints when I show the results of the .artistName and .title match (using contains, because as described above, using equalTo will fail because of the parens in the album title :D)

MusicItemCollection<Album>(
  items: [
    Album(id: "-8844999249623188175", title: "Abandoned (Deluxe Edition)", artistName: "Defeater")
  ]
)

Should I be converting the MPMediaItemPropertyAlbumPersistentID to a string like this, or is there another to instantiate it as a MusicItemID?


FB10582021 Expose catalogID if present for Library albums Being able to use an album’s MPMediaItemPropertyAlbumPersistentID to fetch the library version is great, but unless I’m missing something, you then lose access to the album's Apple Music catalog ID. It would be great to be able to still access the catalog ID as well as the local ID.

Running dump() on the fetched object, it looks like the catalog ID is embedded in some of the properties, such as the Play Parameters.

 ▿ playParameters: Optional(MusicKit.PlayParameters(id: -8844999249623188175, kind: "album", isLibrary: Optional(true), catalogID: Optional(MusicKit.MusicCatalogID(value: 1485062012, kind: MusicKit.MusicCatalogID.Kind.adamID)), deviceLocalID: Optional(MusicKit.MusicDeviceLocalID(value: -8844999249623188175, databaseID: 9C47D30D-37C1-4F5C-B5AF-010C9A1052F1)), rawValues: [:]))

As always, please let me know if I can provide any other information here or in the Feedbacks.

Hello @talkingsmall,

Thanks for looking at these new APIs so promptly, and for filing all those detailed tickets on Feedback Assistant.

The first two look like things we need to investigate further before we can make any kind of comment on them.

The last one about exposing the catalog id is one which, to be honest, I'm not convinced we want to do. We don't want to expose data just for the sake of it, just because we can. When we expose data, we have some idea of what we think it's going to be useful for.

Furthermore, the matter of identifiers with MusicKit is a fairly tricky one, and one we think deeply about. I'm sure there's always room for improvement, but we've seen several instances where developers get confused about the type of id they're supposed to pass to various APIs, and that leads us to believe that we can usually expose our APIs in a way that removes the possibility of anyone making a mistake about the identifiers they pass to MusicKit. We typically do that by leveraging Swift's strongly typed system at compile time, by requiring whole music items in various APIs.

As an example, that's why we strongly encourage the use of methods of MusicLibraryRequest like filter(matching:contains:) for relationships.

More generally, there are many things we've seen developers do with the catalog id values that can be achieved in different ways with MusicKit; sometimes, these alternatives are more expressive and idiomatic in Swift, and they can lead to more flexibility for the framework to have more data that might enable it to perform the work faster in some scenarios.

Therefore, we definitely need to know more about why you even want to get the catalog id.

Thank you very much for your continued feedback!

Best regards,

@JoeKun

Got it -- that makes sense. Indeed, thanks to the excellent API design, many of the use-cases I can think of are covered by other functions, such as being able to use with(_:preferredSource:) to pull in Library and Catalog information in one fell swoop.

The one thing I can't find a solution for is being able to get a link to share the catalog version of the library album or song resource. The URL property seems to always return nil. Perhaps that could still be populated with the link to the resource using the catalog ID (if available for a given resource)?

Is your expectation that the /me/library/albums/[id]?relate=catalog API endpoint should still work using this new ID approach? I'm able to return a result for a library song using the below MusicDataRequest, but I notice that the id for a library song is still the the old school i.XXXXXXXX format:

MusicDataRequest(urlRequest: URLRequest(url: URL(string: "https://api.music.apple.com/v1/me/library/songs/i.XMDXXbxtOl0kZ84?relate=catalog")!))

Now that the underlying MPMediaItemPropertyAlbumPersistentID is being used as the local ID for albums, the below MusicDataRequest returns a "Resource with Requested id was not found" error.

MusicDataRequest(urlRequest: URLRequest(url: URL(string: "https://api.music.apple.com/v1/me/library/albums/-8844999249623188175?relate=catalog")!))

One way or another, it would be very helpful to have a reliable way of translating between catalog and local IDs.


On another note, I've made a little more progress on investigating the issue of some MPMediaItemPropertyAlbumPersistentIDs not returning an album. If I modify my code to convert the unsignedUInt64 returned from the albumPersistentID to a signed Int64 as follows, I'm able to successfully return an album for any ID I give it. It's easy enough to do, but probably confusing for folks not expecting to have to do that.

let idString = String(Int64(bitPattern: rep.albumPersistentID))

Thanks!

Hi @talkingsmall,

The one thing I can't find a solution for is being able to get a link to share the catalog version of the library album or song resource. The URL property seems to always return nil.

Funny you should say that; we've been discussing this exact scenario this week for our internal clients.

Perhaps that could still be populated with the link to the resource using the catalog ID (if available for a given resource)?

I believe that is the desired solution from an API standpoint.

I'd actually strongly discourage you from using MusicDataRequest with a custom crafted URL based on the id of the Album. As much as we are trying to finalize our heuristics for how to pick the correct id for each type during the beta period for iOS 16, I must also say that we reserve the right to change this heuristic in the future.

Instead, I would simply request that you file a ticket on Feedback Assistant asking for the url property to be set to a non-nil value for items coming from the music library.

Thank you very much in advance.

Best regards,

  • You got it. Just made FB10590844. Thanks!

    I'm not totally sure I follow the bit about being discouraged from using MusicDataRequest in the way I describe above. Isn't the /me/library/albums/[id]?relate=catalog endpoint the supported way to switch between catalog and library IDs, as described in last year's "Cross Reference content with the Apple Music API" talk?

Add a Comment

Just bumping this thread to note that FB10581868 has been resolved as of beta 4 and I no longer see failures for negative MPMediaItemPropertyAlbumPersistentID values. Hooray! Thanks for that.

The other feedbacks mentioned this thread are still unresolved. 🤞