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 98 99 static func isStatusPage(response: HTTPURLResponse) -> Bool {100 !(response.mimeType ?? "").contains("json")101 }102103 private func performSearch(queryItems: [URLQueryItem]) async throws -> ([Wallpaper], Meta?) {104 var urlComponents = URLComponents(105 url: Configuration.baseURL.appendingPathComponent("search"), resolvingAgainstBaseURL: true)!106 urlComponents.queryItems = queryItems107108 let url = urlComponents.url!109110 let (data, response) = try await session.data(from: url)111112 guard let httpResponse = response as? HTTPURLResponse else {113 throw WallhavenError.apiError("Wallhaven returned an unexpected response.")114 }115116 guard (200...299).contains(httpResponse.statusCode) else {117 throw WallhavenError.apiError(Self.message(forStatus: httpResponse.statusCode))118 }119120 121 122 123 guard !Self.isStatusPage(response: httpResponse) else {124 throw WallhavenError.apiError(125 "Wallhaven is offline right now — it's serving its status page.")126 }127128 do {129 let decodedResponse = try JSONDecoder().decode(WallhavenResponse.self, from: data)130 return (decodedResponse.data, decodedResponse.meta)131 } catch {132 throw WallhavenError.decodingError(error)133 }134 }135}