jsdw_ios/QuickLocation/Section/AppRestrict/ITunesSearchService.swift

67 lines
2.1 KiB
Swift

//
// 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<String>()
var merged: [AppCatalogItem] = []
for item in local + remote {
guard !seen.contains(item.id) else { continue }
seen.insert(item.id)
merged.append(item)
}
return merged
}
}