Search the iTunes catalog from Swift.

A Swift concurrency client for Apple's iTunes Search API. Typed entity and media searches, validated request options, and a per-client rate limit.

Swift 6.2 Platforms: iOS 16+, macOS 13+ License: MIT CI status Code coverage

Quick start

import TunesSearchKit

let response = try await TunesClient.shared.song("Indigo - Sam Barber")

for song in response.results {
  print(song.trackName)
}

Every search returns a TunesResponse<T>, where T is the model for that entity. Calls are async and throws, so use them from an async context or a Task.

Look up by ID

When you already know a catalog identifier, look up an item by its iTunes ID, AMG ID, UPC/EAN, or ISBN. Related entity lookups can return mixed result shapes, which are represented by LookupResult.

let response = try await TunesClient.shared.lookup(
  .amgArtistId([468749, 5723]),
  entity: .album,
  limit: 5,
  sort: .recent
)

for result in response.results {
  if case .album(let album) = result {
    print(album.collectionName)
  }
}

Install

Requires Swift 6.2 or later, and iOS 16 or later or macOS 13 or later. Add the package to your Package.swift:

dependencies: [
  .package(url: "https://github.com/YasserB94/TunesSearchKit.git", branch: "main")
]

Then add the product to your target:

.product(name: "TunesSearchKit", package: "tunessearchkit")

What you get

Typed entity search

Use entity or its convenience methods, like song or software, when you want one concrete result type.

Media search

Use media, all, music, or tv to search a whole category and switch over mixed results.

Built-in rate limiting

Each client allows up to 20 requests per minute. It throws TunesError.rateLimited(seconds) when its limit is reached.

Validated requests

Bad parameters throw TunesError.invalidParameter before any request is sent.

Supported typed entity queries

Descriptor Result type
.song Song
.album Album
.musicArtist MusicArtist
.musicTrack MusicTrack (song or music video)
.musicVideo MusicVideo
.podcast Podcast
.audiobook Audiobook
.tvEpisode, .tvSeason TVEpisode, TVSeason
.software Software
.ebook Ebook

Apple documents additional entities, including movie, movieArtist, shortFilm, shortFilmArtist, podcastAuthor, audiobookAuthor, and mix. These entity searches returned no results during fixture generation, so TunesSearchKit does not currently provide typed entity queries for them. The feature-movie kind is supported inside AllTrack when returned by an all-media search.

Media searches and result types

Media searches decode each response row into the result enum shown below. The enum cases reflect the response shapes handled by the current decoders.

Media query Result type and supported cases
.all AllMedia: album, artist, podcast, music video, audiobook, TV season, and AllTrack results. AllTrack can decode songs, music videos, podcasts, feature movies, and TV episodes.
.music MusicMedia: music artist, album, and MusicTrack (song or music video).
.musicVideo MusicVideoMedia: music artist or music video.
.podcast PodcastMedia: podcast.
.audiobook AudiobookMedia: audiobook.
.tv TVMedia: TV episode or season.
.software SoftwareMedia: software. iPad and Mac software use the same result case.
.ebook EbookMedia: ebook.

For example, an all-media result may be an album or artist directly, or a track-shaped result whose AllTrack case identifies the concrete model:

let response = try await TunesClient.shared.all("Faithless", limit: 10)

for result in response.results {
  switch result {
  case .album(let album):
    print("Album: \(album.collectionName)")
  case .allArtist(let artist):
    print("Artist: \(artist.artistName)")
  case .allTrack(let track):
    switch track {
    case .song(let song): print("Song: \(song.trackName)")
    case .musicVideo(let video): print("Video: \(video.trackName)")
    case .podcast(let podcast): print("Podcast: \(podcast.trackName)")
    case .featureMovie(let movie): print("Movie: \(movie.trackName ?? "Untitled")")
    case .tvEpisode(let episode): print("Episode: \(episode.trackName)")
    @unknown default:
      break
    }
  default:
    break
  }
}

Request options

let response = try await TunesClient.shared.song(
  "Faithless",
  country: "BE",
  limit: 10,
  explicit: false
)
Option Default Valid values
country US Two-letter country code
limit 50 1...200
lang en_us en_us, ja_jp
version 2 1, 2
explicit true true, false

Use it in SwiftUI

struct SongList: View {
  let query: String
  @State private var songs: [Song] = []

  var body: some View {
    List(songs, id: \.trackId) { song in
      Text(song.trackName)
    }
    .task {
      songs = (try? await TunesClient.shared.song(query).results) ?? []
    }
  }
}

Go deeper

The full API reference covers every model, the media search enums, and all error cases. The contributing guide covers the repository layout, running tests and lint locally, and generating fixtures.