Last active
February 16, 2025 11:54
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 📄 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