Experimenting with MusicKit for Swift - Search Library Resources

If you’re creating an app that displays the local resources in your library, there are chances that you may want to implement a search functionality as well. In the Yours tab of my app, Musadora, I added the search functionality using the latest searchable modifier in SwiftUI 3.0, leveraging the simplicity offered by MusicKit.

Endpoint

Apple Music API offers us the following endpoint to search the library using a query -

GET https://api.music.apple.com/v1/me/library/search

For example, I want to search for the song “Twenty Eight” by The Weeknd while writing this post. I append the required term query to the endpoint to search for a particular term. As there’s a space between the term, I’ll have to convert the spaces into a plus sign(+) as well. The endpoint will look like this -

GET https://api.music.apple.com/v1/me/library/search?term=twenty+eight

The search can return different resources, so I’ve to be specific about the resources to include in the results. For this, we’ve required query parameter, types, which is an array of strings. The possible values are -

  • library-albums
  • library-songs
  • library-playlists
  • library-artists
  • library-music-videos

As I know I’m looking for a song, I’ll append it to the endpoint -

GET https://api.music.apple.com/v1/me/library/search?term=twenty+eight&types=library-songs

There are a few optional query parameters like limit to return a particular number of objects or the number of objects in the specified relationship. The default value is 5, with a maximum limit of 25. There’s another, offset, to fetch the next page or group of objects.

Now that we know the URL, it’s time to get the response!

Response

First, I created a mock function to see the data that we’re getting from the response. Based on that, I’ll create the data model. I love the new concurrency model in Swift 5.5, so I will use the async/await syntax.

private func search(term: String) async throws {
    let termValue = term.replacingOccurrences(of: " ", with: "+")

    var requestURLComponents = URLComponents()
    requestURLComponents.scheme = "https"
    requestURLComponents.host = "api.music.apple.com"
    requestURLComponents.path = "/v1/me/library/search"

    requestURLComponents.queryItems = [
        URLQueryItem(name: "term", value: termValue),
        URLQueryItem(name: "types", value: "library-songs")
    ]

    guard let url = requestURLComponents.url else { return }

    let request = MusicDataRequest(urlRequest: URLRequest(url: url))
    let response = try await request.response()

    print(response.debugDescription)
}

The debugDescription variable of MusicDataResponse gives a detailed description of the URL response and the JSON in a pretty printed format. (Thanks for the tip @Joel!)

The response we get back is LibrarySearchResponse, which has the results property containing all the different resources. In our case, we get back a library-songs property containing the library songs results.

Earlier, we used to create our own data model for the Song object, but fortunately, MusicKit provides a far better way. We can model it as a MusicItemCollection<Song> that is a collection of songs. It benefits us from using the native structures defined in MusicKit like Song and Artwork.

The data model looks like -

struct LibrarySearchResponse: Codable {
    let results: LibrarySearchResults
}

struct LibrarySearchResults: Codable {
    let librarySongs: MusicItemCollection<Song>

    enum CodingKeys: String, CodingKey {
        case librarySongs = "library-songs"
    }
}

Now, I want to search for “The Weeknd” and fetch both the library-songs and library-artists. The endpoint will be -

GET https://api.music.apple.com/v1/me/library/search?term=weeknd&types=library-songs,library-artists

If we run the code and try to decode it -

let model = try JSONDecoder().decode(LibrarySearchResponse.self, from: response.data)
print(model)    

We’ll get an error as I constrained LibrarySearchResults for songs items only. To overcome this limitation and fetch any resource type, I’ll update LibrarySearchResults with the different library resource types.

For convenience, I created a few typealias for better readability.

public typealias Songs = MusicItemCollection<Song>
public typealias Artists = MusicItemCollection<Artist>
public typealias Albums = MusicItemCollection<Album>
public typealias MusicVideos = MusicItemCollection<MusicVideo>
public typealias Playlists = MusicItemCollection<Playlist>

public enum MusicLibrarySearchType: String, CodingKey {
    case songs = "library-songs"
    case artists = "library-artists"
    case albums = "library-albums"
    case musicVideos = "library-music-videos"
    case playlists = "library-playlists"
}

public struct MusicLibrarySearchResponse {
    public let songs: Songs
    public let artists: Artists
    public let albums: Albums
    public let musicVideos: MusicVideos
    public let playlists: Playlists
}

extension MusicLibrarySearchResponse: Decodable {
    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: MusicLibrarySearchType.self)

        songs = try container.decodeIfPresent(Songs.self, forKey: .songs) ?? []
        artists = try container.decodeIfPresent(Artists.self, forKey: .artists) ?? []
        albums = try container.decodeIfPresent(Albums.self, forKey: .albums) ?? []
        musicVideos = try container.decodeIfPresent(MusicVideos.self, forKey: .musicVideos) ?? []
        playlists = try container.decodeIfPresent(Playlists.self, forKey: .playlists) ?? []
    }
}

struct MusicLibrarySearchResponseResults: Decodable {
    var results: MusicLibrarySearchResponse
}

Now, if we decode using MusicLibrarySearchResponseResults, we’ll get the songs and artists as well!

While there’s a MusicCatalogSearchRequest for searching the Apple Music catalog, there’s nothing for natively searching the local library. I’m working on a framework, MusadoraKit, for bridging the gap and providing a more accessible wrapper over MusicKit. I created MusicLibrarySearchRequest which works similar to MusicCatalogSearchRequest -

let request = MusicLibrarySearchRequest(term: "the weeknd", types: [Artist.self, Song.self])
let response = try await request.response()

print(response.songs)
print(response.artists)

You can find the detailed documentation - MusicCatalogSearchRequest

Note: This framework is in alpha state.

Thanks for reading this post!