1import Foundation23enum WallhavenError: Error, LocalizedError {4 case invalidURL5 case networkError(Error)6 case decodingError(Error)7 case noData8 case apiError(String)910 11 12 13 var errorDescription: String? {14 switch self {15 case .invalidURL:16 return "That isn't a valid Wallhaven URL."17 case .networkError(let error):18 return "Couldn't reach Wallhaven: \(error.localizedDescription)"19 case .decodingError:20 return "Wallhaven sent a response Haven couldn't read."21 case .noData:22 return "Wallhaven returned no data."23 case .apiError(let message):24 return message25 }26 }27}28293031enum WallhavenSorting: String {32 case dateAdded = "date_added"33 case toplist34 case random35}3637@MainActor38protocol WallhavenSearching {39 func searchWallpapers(40 query: String, categories: String, purity: String, sorting: WallhavenSorting,41 resolutions: [String], ratios: [String], limit: Int, page: Int42 ) async throws -> ([Wallpaper], Meta?)43}4445class WallhavenAPI: WallhavenSearching {46 private let session: URLSession4748 init(session: URLSession = .shared) {49 self.session = session50 }5152 func searchWallpapers(53 query: String,54 categories: String = "111",55 purity: String = "100",56 sorting: WallhavenSorting = .dateAdded,57 resolutions: [String] = [],58 ratios: [String] = [],59 limit: Int = 24,60 page: Int = 161 ) async throws -> ([Wallpaper], Meta?) {62 var queryItems = [63 URLQueryItem(name: "q", value: query),64 URLQueryItem(name: "categories", value: categories),65 URLQueryItem(name: "purity", value: purity),66 URLQueryItem(name: "sorting", value: sorting.rawValue),67 URLQueryItem(name: "limit", value: String(limit)),68 URLQueryItem(name: "page", value: String(page)),69 ]7071 if !resolutions.isEmpty {72 queryItems.append(73 URLQueryItem(name: "resolutions", value: resolutions.joined(separator: ",")))74 }7576 if !ratios.isEmpty {77 queryItems.append(URLQueryItem(name: "ratios", value: ratios.joined(separator: ",")))78 }7980 return try await performSearch(queryItems: queryItems)81 }8283 84 85 86 static func message(forStatus status: Int) -> String {87 switch status {88 case 429:89 return "Wallhaven rate limit reached (HTTP 429). Try again in a minute."90 case 500...:91 return "Wallhaven is unavailable right now (HTTP \(status))."92 default:93 return "Wallhaven rejected the request (HTTP \(status))."94 }95 }9697 private func performSearch(queryItems: [URLQueryItem]) async throws -> ([Wallpaper], Meta?) {98 var urlComponents = URLComponents(99 url: Configuration.baseURL.appendingPathComponent("search"), resolvingAgainstBaseURL: true)!100 urlComponents.queryItems = queryItems101102 let url = urlComponents.url!103104 let (data, response) = try await session.data(from: url)105106 guard let httpResponse = response as? HTTPURLResponse else {107 throw WallhavenError.apiError("Wallhaven returned an unexpected response.")108 }109110 guard (200...299).contains(httpResponse.statusCode) else {111 throw WallhavenError.apiError(Self.message(forStatus: httpResponse.statusCode))112 }113114 do {115 let decodedResponse = try JSONDecoder().decode(WallhavenResponse.self, from: data)116 return (decodedResponse.data, decodedResponse.meta)117 } catch {118 throw WallhavenError.decodingError(error)119 }120 }121}