Skip to content

Instantly share code, notes, and snippets.

@bennokress
Last active February 16, 2025 11:54
Show Gist options
  • Save bennokress/54184b9a097f73d3971a74a377430931 to your computer and use it in GitHub Desktop.
Save bennokress/54184b9a097f73d3971a74a377430931 to your computer and use it in GitHub Desktop.
A first draft for an iTunes Search & Lookup AppIntent usable in the Shortcuts app → see https://iosdev.space/@benno/114013423388202356 for details
//
// 📄 iTunes Search.swift
// 👨🏼‍💻 Author: Benno Kress
//
import AppIntents
import Foundation
// ⚠️ This won't run on its own since API Calls and a few utility methods are defined in other files!
struct ITunesSearch: AppIntent {
static var title: LocalizedStringResource = "Search iTunes"
static var description = IntentDescription("This action will return a iTunes Media JSON fot the given parameters.", categoryName: "iTunes Media", searchKeywords: ["Apple Music", "Music"])
static var openAppWhenRun = false
static var authenticationPolicy: IntentAuthenticationPolicy = .alwaysAllowed
@Parameter(title: "Search Mode", description: "How do you want to query iTunes", default: .parameters)
var searchMode: ITunesSearchMode
@Parameter(title: "Title")
var title: String
@Parameter(title: "Artist")
var artist: String
@Parameter(title: "Album")
var album: String
@Parameter(title: "Apple Music ID")
var appleMusicID: String
@Parameter(title: "Country Code", description: "The Country Code needs the be the one you use Apple Music in.", default: "de")
var countryCode: String
@Parameter(title: "Result Limit", description: "How many results should be returned?", default: 25)
var resultLimit: Int
static var parameterSummary: some ParameterSummary {
Switch(\.$searchMode) {
Case(.id) {
Summary("Search iTunes API with \(\.$searchMode): \(\.$appleMusicID)") {
\.$countryCode
}
}
DefaultCase {
Summary("Search iTunes API with \(\.$searchMode): \(\.$title) by \(\.$artist) included in \(\.$album)") {
\.$resultLimit
\.$countryCode
}
}
}
}
func perform() async throws -> some IntentResult & ReturnsValue<[ITunesMediaInformation]> {
logD("Performing iTunes Search Action", category: .debug)
let iTunesAPIResponse: ITunesAPIResponse = searchMode == .parameters ? try await searchITunes(title: title, artist: artist, album: album, resultLimit: resultLimit, countryCode: countryCode) : try await lookupOnITunes(appleMusicID: appleMusicID, resultLimit: resultLimit, countryCode: countryCode)
let iTunesMediaInformation = parseITunesResponse(response: iTunesAPIResponse)
return .result(value: iTunesMediaInformation)
}
private func lookupOnITunes(appleMusicID: String, resultLimit: Int, countryCode: String) async throws -> ITunesAPIResponse {
// …
}
private func searchITunes(title: String, artist: String, album: String, resultLimit: Int, countryCode: String) async throws -> ITunesAPIResponse {
// …
}
private func parseITunesResponse(response: ITunesAPIResponse) -> [ITunesMediaInformation] {
guard response.resultCount > 0 else { logD("No results found", category: .debug); return [] }
let searchParameter: ITunesSearchParameter = searchMode == .id ? .id(appleMusicID: appleMusicID) : .parameters(title: title, artist: artist, album: album)
return response
.results
.map { ITunesMediaInformation(from: $0, for: searchParameter) }
.sorted { $0.matchConfidence > $1.matchConfidence }
}
}
enum ITunesSearchMode: String, AppEnum, CustomStringConvertible {
case parameters
case id
static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Search Mode")
static var caseDisplayRepresentations: [Self: DisplayRepresentation] = [
.parameters: DisplayRepresentation(title: "Search Parameters", image: .init(systemName: "list.triangle")),
.id: DisplayRepresentation(title: "Apple Music ID", image: .init(systemName: "number"))
]
var description: String {
switch self {
case .parameters: return "Song Parameters"
case .id: return "Apple Music ID"
}
}
}
struct ITunesMediaInformation: AppEntity {
let id: String
@Property(title: "Song Title")
var title: String
@Property(title: "Artist")
var artist: String
@Property(title: "Album Title")
var album: String
@Property(title: "Artwork URL")
var artworkURL: URL
@Property(title: "Release Year")
var releaseYear: Int
@Property(title: "Is Explicit?")
var isExplicit: Bool
@Property(title: "Apple Music URL")
var appleMusicURL: URL
@Property(title: "Apple Music URL Scheme")
var appleMusicURLScheme: URL
@Property(title: "Match Confidence")
var matchConfidence: Int
init(id: String = UUID().uuidString, title: String, artist: String, album: String, artworkURL: URL, releaseYear: Int, isExplicit: Bool, appleMusicURL: URL, appleMusicURLScheme: URL, matchConfidence: Int) {
self.id = id
self.title = title
self.artist = artist
self.album = album
self.artworkURL = artworkURL
self.releaseYear = releaseYear
self.isExplicit = isExplicit
self.appleMusicURL = appleMusicURL
self.appleMusicURLScheme = appleMusicURLScheme
self.matchConfidence = matchConfidence
}
init(from iTunesAPIResult: ITunesAPIResult, for searchParameter: ITunesSearchParameter) {
self.id = String(iTunesAPIResult.titleID)
self.title = iTunesAPIResult.title
self.artist = iTunesAPIResult.artist
self.album = iTunesAPIResult.album
self.artworkURL = iTunesAPIResult.artworkURL
self.releaseYear = iTunesAPIResult.releaseYear
self.isExplicit = iTunesAPIResult.isExplicit
self.appleMusicURL = iTunesAPIResult.appleMusicURL
self.appleMusicURLScheme = iTunesAPIResult.appleMusicURLScheme
self.matchConfidence = searchParameter.resultConfidence(for: iTunesAPIResult.title, iTunesAPIResult.artist, and: iTunesAPIResult.album)
logD("\(matchConfidence)% -> \(artist) - \(title) (\(album))", category: .debug)
}
static let defaultQuery = ITunesMediaInformationIntentQuery()
static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "iTunes Media Information")
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(
title: "\(title) by \(artist)",
subtitle: "Match Confidence: \(matchConfidence)%"
)
}
}
struct ITunesMediaInformationIntentQuery: EntityQuery {
// This would only be useful if we had locally saved iTunes Media Information Objects that we could query.
func entities(for identifiers: [String]) async throws -> [ITunesMediaInformation] { [] }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment