Searching for a track with MusicKit Swift Beta

I'm trying to perform a search for a song by album, artist, & title in the cases when I don't know the song's id (or if there is no isrc match)

Reading the docs, it seems that I can only search for one term at a time, but against a list of MusicCatalogSearchable Types

So my strategy would be to search for albums by name, then filter the results by artist and title.

Two questions here, really:

  1. Is the above a good strategy, or is there a better way to do this?

  2. For the life of my I cannot figure out from the docs, what to put in place of [MusicCatalogSearchable.Album] in the below.

var albumRequest = MusicCatalogSearchRequest(term: album.name, types: [MusicCatalogSearchable.Album])

Xcode doesn't like the above and gives the following error:

Type 'MusicCatalogSearchable' has no member 'Album'

Sadly, everything I've tried, results in an error.

When I look in MusicKit, I see the below, which seems empty to me:

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
public protocol MusicCatalogSearchable : MusicItem {
}

What am I missing?

Accepted Reply

Hello @Kimfucious,

Indeed, MusicCatalogSearchRequest doesn't currently have something akin to MusicCatalogResourceRequest's properties; this is partly due to the fact that you can search for more than one type at a time, so exposing the corresponding concept for search would require a more complicated API; but it seems like you have an interesting use-case for this, so feel free to file a ticket about this on Feedback Assistant.

The first couple of steps you described seem fine to me.

The third step can be simplified into a single request like this:

var detailedAlbumsRequest = MusicCatalogResourceRequest<Album>(matching: \.id, memberOf: matchedAlbums.map(\.id))
detailedAlbumsRequest.properties = [.tracks]

let detailedAlbumsResponse = try await detailedAlbumsRequest.response()
let detailedAlbums = detailedAlbumsResponse.items

Then the rest is also pretty straightforward. You can essentially do something like this:

let matchingTracks = detailedAlbums.flatMap { album -> [Track] in
    let tracks = album.tracks ?? []
    return tracks.filter { track in
        return track.title.hasPrefix(songTitle)
    }
}

I tested this briefly, and I was able to get results like these very easily:

Found: Track.song(Song(id: "697195787", title: "Harder Better Faster Stronger", artistName: "Daft Punk"))
Found: Track.song(Song(id: "703078830", title: "Harder Better Faster Stronger", artistName: "Daft Punk"))
Found: Track.song(Song(id: "696669452", title: "Harder Better Faster Stronger (The Neptunes Remix)", artistName: "Daft Punk"))
Found: Track.song(Song(id: "696669894", title: "Harder Better Faster Stronger (Jess and Crabbe Mix)", artistName: "Daft Punk"))

I understand your point about Track, but this type is important because an Album can contain a collection of items which can be either songs or music videos; the same goes for Playlist.

Converting a collection of tracks into a collection of songs is actually pretty easy though:

let matchingSongs = matchingTracks.compactMap { track -> Song? in
    guard case .song(let song) = track else { return nil }
    return song
}

That said, we did go the extra mile to make Track as useful as possible without having to unwrap its underlying Song or MusicVideo, by exposing directly at the Track-level all the common properties that are present in both of these underlying item types. That's how I was able to use the track's title above, without trying to get the underlying song.

Furthermore, an instance of Track is just as convenient as an instance of Song for other common tasks, such as initiating playback with MusicKit's ApplicationMusicPlayer or SystemMusicPlayer.

So if you really find it so tedious to convert a collection of tracks into a collection of songs, then I would encourage you to consider using Track in your code as the result type for your function. You might find it just as functional as Song for your purposes. As for bridging the gap for your other code-path where you find a Song by isrc (let's call it matchedSong), converting that into a Track is as easy as doing:

let matchedTrack = Track.song(matchedSong)

I hope this helps.

Best regards,

Replies

Hello @Kimfucious,

Regarding your first question, I'm not a real expert of the capabilities of Apple Music API's search endpoint, but my impression is that your strategy is a fine starting point. I suggest you go ahead with that for now, and see if that works out in practice. If not, then maybe you can come back to us in the forums with more specific information about what you think you would need from this search API for your use case.

As for your second question, all you have to do is:

var albumRequest = MusicCatalogSearchRequest(term: album.name, types: [Album.self])

Also, I suggest you take a look at our sample app for MusicKit. It showcases all the most important core concepts of MusicKit for Swift, including how to use both structured requests offered by the framework.

I hope this helps.

Best regards,

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:

  1. Get a list of albums based on a search string
  2. 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.

  1. 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.

  1. 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.
  • numbering in markdown not quite working here.

Add a Comment

Hello @Kimfucious,

Indeed, MusicCatalogSearchRequest doesn't currently have something akin to MusicCatalogResourceRequest's properties; this is partly due to the fact that you can search for more than one type at a time, so exposing the corresponding concept for search would require a more complicated API; but it seems like you have an interesting use-case for this, so feel free to file a ticket about this on Feedback Assistant.

The first couple of steps you described seem fine to me.

The third step can be simplified into a single request like this:

var detailedAlbumsRequest = MusicCatalogResourceRequest<Album>(matching: \.id, memberOf: matchedAlbums.map(\.id))
detailedAlbumsRequest.properties = [.tracks]

let detailedAlbumsResponse = try await detailedAlbumsRequest.response()
let detailedAlbums = detailedAlbumsResponse.items

Then the rest is also pretty straightforward. You can essentially do something like this:

let matchingTracks = detailedAlbums.flatMap { album -> [Track] in
    let tracks = album.tracks ?? []
    return tracks.filter { track in
        return track.title.hasPrefix(songTitle)
    }
}

I tested this briefly, and I was able to get results like these very easily:

Found: Track.song(Song(id: "697195787", title: "Harder Better Faster Stronger", artistName: "Daft Punk"))
Found: Track.song(Song(id: "703078830", title: "Harder Better Faster Stronger", artistName: "Daft Punk"))
Found: Track.song(Song(id: "696669452", title: "Harder Better Faster Stronger (The Neptunes Remix)", artistName: "Daft Punk"))
Found: Track.song(Song(id: "696669894", title: "Harder Better Faster Stronger (Jess and Crabbe Mix)", artistName: "Daft Punk"))

I understand your point about Track, but this type is important because an Album can contain a collection of items which can be either songs or music videos; the same goes for Playlist.

Converting a collection of tracks into a collection of songs is actually pretty easy though:

let matchingSongs = matchingTracks.compactMap { track -> Song? in
    guard case .song(let song) = track else { return nil }
    return song
}

That said, we did go the extra mile to make Track as useful as possible without having to unwrap its underlying Song or MusicVideo, by exposing directly at the Track-level all the common properties that are present in both of these underlying item types. That's how I was able to use the track's title above, without trying to get the underlying song.

Furthermore, an instance of Track is just as convenient as an instance of Song for other common tasks, such as initiating playback with MusicKit's ApplicationMusicPlayer or SystemMusicPlayer.

So if you really find it so tedious to convert a collection of tracks into a collection of songs, then I would encourage you to consider using Track in your code as the result type for your function. You might find it just as functional as Song for your purposes. As for bridging the gap for your other code-path where you find a Song by isrc (let's call it matchedSong), converting that into a Track is as easy as doing:

let matchedTrack = Track.song(matchedSong)

I hope this helps.

Best regards,

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:

  1. 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
}
  1. 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
    }
  1. 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
    }