// // ITunesSearchService.swift // QuickLocation // import Foundation enum ITunesSearchError: Error { case invalidURL case invalidResponse } enum ITunesSearchService { private struct SearchResponse: Decodable { let results: [ITunesAppResult] } private struct ITunesAppResult: Decodable { let trackId: Int let trackName: String let artworkUrl100: String? let artworkUrl512: String? } static func search(term: String, limit: Int = 25) async throws -> [AppCatalogItem] { let trimmed = term.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { return [] } var components = URLComponents(string: "https://itunes.apple.com/search") components?.queryItems = [ URLQueryItem(name: "term", value: trimmed), URLQueryItem(name: "entity", value: "software"), URLQueryItem(name: "country", value: "cn"), URLQueryItem(name: "limit", value: "\(limit)") ] guard let url = components?.url else { throw ITunesSearchError.invalidURL } let (data, response) = try await URLSession.shared.data(from: url) guard let http = response as? HTTPURLResponse, (200...299).contains(http.statusCode) else { throw ITunesSearchError.invalidResponse } let decoded = try JSONDecoder().decode(SearchResponse.self, from: data) return decoded.results.map { result in let iconURL = result.artworkUrl512 ?? result.artworkUrl100 return AppCatalogItem( id: "itunes:\(result.trackId)", name: result.trackName, icon: "", keywords: [], iconURL: iconURL ) } } static func mergedResults(local: [AppCatalogItem], remote: [AppCatalogItem]) -> [AppCatalogItem] { var seen = Set() var merged: [AppCatalogItem] = [] for item in local + remote { guard !seen.contains(item.id) else { continue } seen.insert(item.id) merged.append(item) } return merged } }